3 puan yazan GN⁺ 2024-05-09 | 1 yorum | WhatsApp'ta paylaş
  • 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 yield noktasında kontrolü geri alıp sıradaki işe geçmesidir
  • sleep, zaman dolana kadar yield eden bir alt jeneratör olarak kurulabilir; yield from ise alt jeneratör bittiğinde özgün işin devam etmesini sağlar
  • async/await biçimine geçirildiğinde Task.__await__() tamamlanana kadar kontrolü event loop’a devreder; create_task ve run da gerçek asyncio API’sine benzer bir yapı oluşturur
  • Özel jacobio çağrılarını asyncio ile 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 range gibi jeneratörler, tüm diziyi bellekte tutmak yerine gereken değerleri tek tek üretir
    • range(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 yield kullanı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 sonraki yield noktasına kadar çalışır
    • Artık yield kalmadığında StopIteration istisnası 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 yield ile durur ve kontrolü event loop’a geri verir
  • task1() ve task2() örneğinde, her biri çıktı verip ardından yield ettiğinde çıktı dönüşümlü görünür
    • İki fonksiyon da while True döngüsüne sahip olduğu için çalışmaları sürer
    • Çıktı Task 1, Task 2, Task 1, Task 2 şeklinde tekrar eder

yield from ile sleep oluşturmak

  • sleep(seconds), başlangıç zamanını kaydedip belirtilen süre dolana kadar sürekli yield eden bir jeneratör olarak uygulanır
  • İş fonksiyonu, yürütmeyi yield from sleep(1) ya da yield from sleep(5) gibi bir sleep alt jeneratörüne devreder
    • sleep sürekli yield ederken iş durmuş durumda kalır
    • Yeterli süre geçince sleep içindeki while döngüsü sona erer
    • Artık yield kalmadığı için StopIteration oluşur ve yield from iş fonksiyonundaki bir sonraki satırdan devam eder
  • Örnekte task1 her 1 saniyede bir, task2 ise her 5 saniyede bir çıktı verir
    • Çıktı, Task 1, Task 2 sonrasında Task 1’in birkaç kez görünmesi ve sonra yeniden Task 2’nin gelmesi şeklindedir

yield yerine await

  • await kullanmak için hedef nesnenin __await__ metoduna sahip olması ya da bir coroutine olması gerekir
  • asyncio’da genellikle asyncio.create_task gibi fonksiyonlarla Task nesneleri kullanılır
    • Task nesnesi asyncio’nun Future nesnesinden türetilir
    • Future nesnesinde __await__ metodu bulunur
  • Başında async olan 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 bir yield from gibi düşünülebilir
    • await object, nesne örneğinin __await__ içinde yield etmesi ya da başka bir coroutine’i beklemesi anlamına gelir
  • asyncio’nun Future kaynak kodunda da Future veya Task tamamlanmadığında __await__’un temelde yield çağırdığı görülebilir

Basit Task, create_task, run uygulaması

  • Özel uygulama, event loop için liste yerine Queue kullanır
    • Amaç, işleri döngüye ekleme ve silme işlemlerini sabit zamanda yapabilmektir
  • Task sınıfı, jeneratör nesnesini ve tamamlanma durumunu saklar
    • Jeneratör nesnesi self.iter içinde tutulur
    • self.finished başlangıçta False olur
    • Jeneratör StopIteration fırlattığında iş tamamlanmış kabul edilir
    • done() tamamlanma durumunu döndürür
  • Task.__await__(), iş bitene kadar sürekli yield self çağırır
    • Bu davranış, kontrolü event loop’a geri verir
  • create_task(generator), jeneratörü Task ile sarar, event loop kuyruğuna koyar ve geri döndürür
    • Yani işi event loop üzerinde planlar
  • run(main), gerçek asyncio.run() benzeri biçimde event loop’u başlatır
    • İlk alınan main, Task ile sarılıp kuyruğa konur
    • Kuyruk boş olmadığı sürece sıradaki iş alınır
    • task.iter.send(None) ile iş ilerletilir
    • StopIteration oluşursa task.finished = True yapılır
    • İstisna yoksa iş yeniden event loop kuyruğuna eklenir
  • next(task.iter) yerine task.iter.send(None) kullanılmasının nedeni async/await ile çalışmanın özellikleridir; burada fiilen aynı görevi görür

async uyumlu sleep ve jacobio örneği

  • Eski sleep bir jeneratör fonksiyonuydu, ancak await jeneratör fonksiyonlarıyla doğrudan birlikte kullanılamaz
    • await hedefi 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 kadar yield eder
  • async def sleep(seconds), _sleep(seconds) ile bir iş oluşturur ve sonra o işi await eder
    • await task, Task.__await__() çağrısını başlatır
    • İş henüz bitmediyse yield ile kontrol event loop’a verilir
  • Tamamlanan özel dosya jacobio.py şu öğeleri içerir
    • event loop kuyruğu
    • _sleep
    • async sleep
    • Task
    • create_task
    • run
  • Kullanım örneğinde önceki yield from, await ile değiştirilir ve await kullanan fonksiyonlara async eklenir
    • task1 iki kez çıktı verir ve her seferinde jacobio.sleep(1) bekler
    • task2 üç kez çıktı verir ve her seferinde jacobio.sleep(0) bekler
    • main, iki işi oluşturur, ikisini de await eder ve ardından done yazdırır
  • Örnek çıktı sırası Task 1, Task 2, Task 2, Task 2, Task 1, done olur

Gerçek asyncio’ya geçmek

  • Özel jacobio örneğinde jacobio geçen yerlerin tümü asyncio ile 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 await etmek yerine asyncio.gather() gibi fonksiyonlarla birden çok iş birlikte yönetilebilir
  • İlgili yazı olarak handling asyncio tasks like a pro bağlantısı veriliyor

1 yorum

 
GN⁺ 2024-05-09
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.sleep gibi ş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ür
    Uygulama 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 def iç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ı var

    • Kötü bir fikir olabilir ama şu anki gibi tüm çağrıların varsayılan olarak senkron olduğu yaklaşım yerine bunun tersini yapsak nasıl olur diye düşünüyorum
      Aritmetik 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
    • Temporal gerçekten harika
      asyncio olay döngüsünü nasıl uyguladıklarını öğrendiğimde gerçekten kafamda ampul yandı
    • Neden thread kullanmadıklarını merak ediyorum
      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
    • İş akışı örneğinde Purchaser.purchase, do_purchase olmamalı mı?
    • Bunu statik olarak yakalamak kolay olmayacaktır
      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 select zaman aşımıyla birlikte kullanılabiliyor

    • asyncio'nun kendisinde de varsayılan uygulama dışında özel olay döngüsü uygulamaları kullanılabilir
      Ünlü uygulamalardan biri uvloop(https://github.com/MagicStack/uvloop); temelde döngüyü uygulamak için libuv kullanıyor ve tarif ettiğiniz select benzeri işleri libuv hallediyor
    • Bu, SimPy [1] / Simpy.io [2] ile epey benzer duyuluyor ama SimPy asyncio'dan yıllar önce çıkmıştı
      [1] https://simpy.readthedocs.io/en/latest/
      [2] https://gitlab.com/team-simpy/simpy.io
    • Bir açıdan bu yaklaşımda özellikle bir sorun yok
      Olay döngüsünün illa tekrar etmesi gerekmez; maini çalıştırarak başlayıp o bitince onunla birlikte bitebilir. Soket bekleyen bir while true dö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ün
      Olay 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
    • Bunları öğrenmek için en iyi kitap hangisi?
  • 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 yield hiç 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ı yok

  • Python'ın defi ele geçirip, gerçekte fonksiyon olmayan nesneler oluşturmak için kullanması tamamen tuhaf
    En azından başka bir anahtar kelime oluşturabilirdi

    • O gerçekten bir fonksiyon; sadece dönüş tipi başka bir fonksiyona dönüştürülmüş durumda
      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 Iterable ve Awaitable tercih edilir
    • Asıl sorun ne? Yoksa sadece dil saflığı meselesi mi?
    • Bunu bir trajedi olarak görmeye gerek yok
      Geliştirici deneyiminin semantiğini dil saflığı gibi şeylerin önüne koymuşlar gibi duruyor. async fonksiyonun asenkron fonksiyon olması yüzünden hiç kafam karışmadı
    • Python'ın daha büyük sorunu, en başta fonksiyon için def kullanmak zorunda olması; bu yüzden JS arrow function'ları veya C++ lambda'ları gibi inline fonksiyon oluşturamıyorsunuz
      Bu dil saflığı meselesi değil, sadece kullanışsız bir durum
    • Decorator'lardan mı bahsediyorsunuz? Python bağlamında ne demek istediğinizi anlamak epey kafa karıştırıcı