Python asyncio’nun Çalışma Mantığı: Sıfırdan Yeniden Kurmak
(jacobpadilla.com)- Python’un asyncio aracı, I/O-bound programları ele almak için kullanılır; bu yazı da bunun çalışma mantığını jeneratörler üzerinde basit bir uygulamayla yeniden kurarak gösteriyor
- Temel fikir, birden çok işi yöneten event loop’un işleri çalıştırırken
yieldnoktasında kontrolü geri alıp sıradaki işe geçmesidir sleep, zaman dolana kadaryieldeden bir alt jeneratör olarak kurulabilir;yield fromise alt jeneratör bittiğinde özgün işin devam etmesini sağlarasync/awaitbiçimine geçirildiğindeTask.__await__()tamamlanana kadar kontrolü event loop’a devreder;create_taskverunda gerçek asyncio API’sine benzer bir yapı oluşturur- Özel
jacobioçağrılarınıasyncioile değiştirince gerçek paketi kullanan koda dönüşür; ancak gerçek asyncio örnektekinden çok daha karmaşıktır ve iç akışı da kısmen farklıdır
Jeneratörlerle asyncio’nun temel yapısı
- asyncio, Python’da I/O-bound programları işlemek için kullanılır; yazı bunu jeneratör tabanlı, basitleştirilmiş bir modelle yeniden kuruyor
- Python 3+ içindeki
rangegibi jeneratörler, tüm diziyi bellekte tutmak yerine gereken değerleri tek tek üretirrange(100_000_000)bir liste gibi oluşturulursa 100 milyon öğe saklanması gerekir, bu da bellek açısından verimsizdir- Jeneratörler, değerler gerektiğinde üretildiği için tüm diziyi saklamaz
- Jeneratör fonksiyonları normal fonksiyonlar gibi tanımlanır ama
yieldkullanır- Fonksiyon çağrıldığında gövde hemen çalışmaz, bunun yerine bir jeneratör nesnesi döner
next(iterator)çağrılırsa bir sonrakiyieldnoktasına kadar çalışır- Artık
yieldkalmadığındaStopIterationistisnası oluşur
yield from, bir jeneratörün bir alt jeneratör ya da iterable nesne çağırmasına izin verir ve böylece jeneratör zinciri kurulabilir- Buradaki önemli özellik, fonksiyon yürütmesinin durdurulup durum korunarak daha sonra yeniden başlatılabilmesidir
Event loop’u bir listeye indirgemek
- Event loop, asyncio’nun mevcut işleri çalıştıran ve yöneten çekirdeğidir
- Gerçek asyncio event loop’u C ile yazılmıştır, ancak basit modelde mevcut işleri tutan bir liste gibi düşünülebilir
- İlk örnekte işler jeneratör nesneleri olarak ele alınır
- Event loop yöneticisi iş listesini dolaşır
- Her iş için
next(task)çağrılarak yürütme sürdürülür - İş, I/O-bound bir görev gibi beklemesi gereken noktada
yieldile durur ve kontrolü event loop’a geri verir
task1()vetask2()örneğinde, her biri çıktı verip ardındanyieldettiğinde çıktı dönüşümlü görünür- İki fonksiyon da
while Truedöngüsüne sahip olduğu için çalışmaları sürer - Çıktı
Task 1,Task 2,Task 1,Task 2şeklinde tekrar eder
- İki fonksiyon da
yield from ile sleep oluşturmak
sleep(seconds), başlangıç zamanını kaydedip belirtilen süre dolana kadar sürekliyieldeden bir jeneratör olarak uygulanır- İş fonksiyonu, yürütmeyi
yield from sleep(1)ya dayield from sleep(5)gibi bir sleep alt jeneratörüne devredersleepsürekliyieldederken iş durmuş durumda kalır- Yeterli süre geçince
sleepiçindekiwhiledöngüsü sona erer - Artık
yieldkalmadığı içinStopIterationoluşur veyield fromiş fonksiyonundaki bir sonraki satırdan devam eder
- Örnekte
task1her 1 saniyede bir,task2ise her 5 saniyede bir çıktı verir- Çıktı,
Task 1,Task 2sonrasındaTask 1’in birkaç kez görünmesi ve sonra yenidenTask 2’nin gelmesi şeklindedir
- Çıktı,
yield yerine await
awaitkullanmak için hedef nesnenin__await__metoduna sahip olması ya da bir coroutine olması gerekir- asyncio’da genellikle
asyncio.create_taskgibi fonksiyonlarlaTasknesneleri kullanılırTasknesnesi asyncio’nunFuturenesnesinden türetilirFuturenesnesinde__await__metodu bulunur
- Başında
asyncolan bir fonksiyon çağrıldığında bir coroutine nesnesi oluşur- Coroutine de tıpkı jeneratör fonksiyonları gibi yürütmeyi durdurup sonra kaldığı yerden sürdürebilir
await, ek doğrulama kuralları olan biryield fromgibi düşünülebilirawait object, nesne örneğinin__await__içindeyieldetmesi ya da başka bir coroutine’i beklemesi anlamına gelir
- asyncio’nun
Futurekaynak kodunda daFutureveyaTasktamamlanmadığında__await__’un temeldeyieldçağırdığı görülebilir
Basit Task, create_task, run uygulaması
- Özel uygulama, event loop için liste yerine
Queuekullanır- Amaç, işleri döngüye ekleme ve silme işlemlerini sabit zamanda yapabilmektir
Tasksınıfı, jeneratör nesnesini ve tamamlanma durumunu saklar- Jeneratör nesnesi
self.iteriçinde tutulur self.finishedbaşlangıçtaFalseolur- Jeneratör
StopIterationfırlattığında iş tamamlanmış kabul edilir done()tamamlanma durumunu döndürür
- Jeneratör nesnesi
Task.__await__(), iş bitene kadar sürekliyield selfçağırır- Bu davranış, kontrolü event loop’a geri verir
create_task(generator), jeneratörüTaskile sarar, event loop kuyruğuna koyar ve geri döndürür- Yani işi event loop üzerinde planlar
run(main), gerçekasyncio.run()benzeri biçimde event loop’u başlatır- İlk alınan
main,Taskile sarılıp kuyruğa konur - Kuyruk boş olmadığı sürece sıradaki iş alınır
task.iter.send(None)ile iş ilerletilirStopIterationoluşursatask.finished = Trueyapılır- İstisna yoksa iş yeniden event loop kuyruğuna eklenir
- İlk alınan
next(task.iter)yerinetask.iter.send(None)kullanılmasının nedeniasync/awaitile çalışmanın özellikleridir; burada fiilen aynı görevi görür
async uyumlu sleep ve jacobio örneği
- Eski
sleepbir jeneratör fonksiyonuydu, ancakawaitjeneratör fonksiyonlarıyla doğrudan birlikte kullanılamazawaithedefi ya__await__içeren bir nesne ya da bir coroutine fonksiyonu olmalıdır
- Gerçek bekleme mantığı
_sleep(seconds)adlı jeneratöre taşınır_sleep, belirli bir süre geçene kadaryieldeder
async def sleep(seconds),_sleep(seconds)ile bir iş oluşturur ve sonra o işiawaitederawait task,Task.__await__()çağrısını başlatır- İş henüz bitmediyse
yieldile kontrol event loop’a verilir
- Tamamlanan özel dosya
jacobio.pyşu öğeleri içerir- event loop kuyruğu
_sleepasync sleepTaskcreate_taskrun
- Kullanım örneğinde önceki
yield from,awaitile değiştirilir veawaitkullanan fonksiyonlaraasynceklenirtask1iki kez çıktı verir ve her seferindejacobio.sleep(1)beklertask2üç kez çıktı verir ve her seferindejacobio.sleep(0)beklermain, iki işi oluşturur, ikisini deawaiteder ve ardındandoneyazdırır
- Örnek çıktı sırası
Task 1,Task 2,Task 2,Task 2,Task 1,doneolur
Gerçek asyncio’ya geçmek
- Özel
jacobioörneğindejacobiogeçen yerlerin tümüasyncioile değiştirilirse gerçek asyncio paketi kullanan koda dönüşür - Karşılık gelen fonksiyonlar şunlardır
jacobio.sleep()→asyncio.sleep()jacobio.create_task()→asyncio.create_task()jacobio.run()→asyncio.run()
- Gerçek asyncio perde arkasında çok daha fazla iş yapar
- Bu event loop yöneticisi olabildiğince basit tutulmuş bir uygulamadır; asyncio’nun temel fikrini gösterir ama gerçek paketin ölçeği ve karmaşıklığı nedeniyle kaynak kod akışıyla tam olarak aynı değildir
- Gerçek asyncio kullanıldığında, iki işi ayrı ayrı oluşturup ikisini de
awaitetmek yerineasyncio.gather()gibi fonksiyonlarla birden çok iş birlikte yönetilebilir - İlgili yazı olarak handling asyncio tasks like a pro bağlantısı veriliyor
1 yorum
Hacker News yorumları
Asyncio, olay döngüsünü doğrudan kendi yazdığınız bir uygulamayla değiştirmeye izin verir
Temporal Python'da iş akışları, özel bir dayanıklı asyncio olay döngüsü ile ifade edilir; böylece
asyncio.sleepgibi şeyler de dayanıklı zamanlayıcılara dönüşür. Yani kod başka bir makinede devam ettirilebilir, bu yüzden haftalarca sleep etmek bile mümkündürUygulama yöntemi bu yazıda açıklanıyor: https://temporal.io/blog/durable-distributed-asyncio-event-l...
asyncio'nun en büyük sorunu, Python'da senkron bir çağrıyla asyncio thread'ini bloke edip tüm sistemi kolayca durdurabilmeniz. Python'un, çağrı grafiği oluşturarak
async defiçinde thread'i bloke eden bir çağrının doğrudan ya da dolaylı olarak çağrılıp çağrılmadığını tespit eden statik analiz araçlarına çok ihtiyacı varAritmetik işlemler veya veri yapısı erişimi gibi temel işlemler hızlı senkron çağrılar olarak izin listesine alınabilir; olay döngüsü şüpheli derecede hızlı dönüyorsa senkron olması gereken başka işleri de tespit edebilir miyiz acaba
asyncio olay döngüsünü nasıl uyguladıklarını öğrendiğimde gerçekten kafamda ampul yandı
Python'ın gerçekten eşzamanlılık için uygun bir dil olup olmadığını hâlâ anlamaya çalışıyorum. Asyncio bana hep kıl payı ayakta duruyormuş gibi geldi; bana göre C#'ın asenkron uygulaması daha temiz
Purchaser.purchase,do_purchaseolmamalı mı?Basit bir
read()bile descriptor'ın ne olduğuna ve nasıl ayarlandığına bağlı olarak bloke olabilir de olmayabilir de. Bunu statik analizle nasıl tespit edeceksiniz?Bu uygulama sleep ederken busy waiting yapıyor gibi görünüyor. Yani şu anda çalıştırılabilir görev olmasa bile olay döngüsü dönmeye devam ediyor
Eskiden başka bir oyuncak uygulama gördüğümü hatırlıyorum; orada görevlerin bir sonraki çalıştırılabilir zamanını sıralı biçimde izliyor, şu anda çalıştırılabilir görev yoksa olay döngüsünün kendisini sleep'e alıyordu. Gerçek asyncio'nun da bu şekilde çalıştığını anlıyorum
Sonrasında bu yaklaşım, bir sonraki çalıştırılabilirlik koşulunun yalnızca duvar saati zamanı değil, soket gibi şeylere bağımlılıklar da içerebilmesi için genişletiliyor; böylece
selectzaman aşımıyla birlikte kullanılabiliyorÜnlü uygulamalardan biri uvloop(https://github.com/MagicStack/uvloop); temelde döngüyü uygulamak için libuv kullanıyor ve tarif ettiğiniz
selectbenzeri işleri libuv hallediyor[1] https://simpy.readthedocs.io/en/latest/
[2] https://gitlab.com/team-simpy/simpy.io
Olay döngüsünün illa tekrar etmesi gerekmez;
maini çalıştırarak başlayıp o bitince onunla birlikte bitebilir. Soket bekleyen birwhile truedöngüsü olan bir sunucu başlattığınızı, sonlandırma koşulu ya da interrupt geldiğinde bitip programın da kapandığını düşününOlay döngüsü açısından busy waiting yoktur; sleep veya soketlere dokunmaya da gerek yoktur. Bu, tamamlanana kadar çalıştırma ile sonsuza kadar çalıştırma arasındaki farktır
Oyuncak bir döngü yapacaksanız sonsuza kadar çalışan durumları özellikle ele almamak daha iyi olur bence
David Beazley'nin asyncio konuşması harika
Buna dayanarak bir ayrık olay simülasyonu aracı yaptım. asyncio'yu kendiniz uygulayabilmek ve sistem saatini simülasyon zamanı ile değiştirebilmek oldukça hoş
Gerçekten çok iyi bir yazı ve ilk kez okuyan birinin sıkılabileceği kısımları iyi atlayan üst düzey bir açıklama
Sonda bunun içeride gerçekte nasıl çalıştığını anlatan başka kaynaklar eklense daha da iyi olurdu
Yazı aynı şeyi
yieldhiç kullanmadan gösterebilseydi bence daha iyi olurdu. Dürüst olmak gerekirse gerçekten sihir gibi hissettiren kısım orasıPython coroutine'lerine dair çok daha derin bir açıklama burada var: https://aosabook.org/en/500L/a-web-crawler-with-asyncio-coro...
poll()dan hiç bahsedilmiyor mu? Öyleyse asyncio'nun çalışma şekliyle hiç alakası yokPython'ın
defi ele geçirip, gerçekte fonksiyon olmayan nesneler oluşturmak için kullanması tamamen tuhafEn azından başka bir anahtar kelime oluşturabilirdi
Generator veya coroutine döndüren bir biçim ve tip imzası burada görülebilir [1]. Dil seviyesinde sözdizimsel şeker olmasa bile decorator gibi şeylerle saf Python'da da benzeri yapılabilir
[1] https://docs.python.org/3/library/typing.html#typing.Generat...
Ancak tip imzasını akademik ilgi açısından belirttim; pratik tip belirtimlerinde daha basit
IterableveAwaitabletercih edilirGeliştirici deneyiminin semantiğini dil saflığı gibi şeylerin önüne koymuşlar gibi duruyor.
asyncfonksiyonun asenkron fonksiyon olması yüzünden hiç kafam karışmadıdefkullanmak zorunda olması; bu yüzden JS arrow function'ları veya C++ lambda'ları gibi inline fonksiyon oluşturamıyorsunuzBu dil saflığı meselesi değil, sadece kullanışsız bir durum