Vahid
Vahid
خواندن ۱۹ دقیقه·۴ سال پیش

پایتون و coroutineش!

تو دنیای امروز از همزمانی(concurrency) به صورت بسیار گسترده‌ای استفاده میشه. شناخت جنبه‌های مختلف این موضوع هم به ما کمک می‌کنه تا موقع تصمیم‌گیری بهتر عمل کنیم. coroutine یکی از مواردیه که میشه از اون در همزمانی استفاده کرد، به خاطر همین تو این پست سعی می‌کنیم که ابتدا با مفهوم coroutine بیشتر آشنا بشیم و بعد از اون بریم سراغ پایتون و ببینیم که پایتون چطوری این موضوع رو مدیریت کرده و چطور باید ازش استفاده کنیم؟ امیدوارم که مفید باشد.

برای اینکه این پست برای همه قابل فهم باشه توی بخش اول به توضیح بعضی مفاهیم می‌پردازیم و بعد از اون به سراغ پیاده‌سازی اون تو پایتون می‌ریم. پس اگر با این مفاهیم آشنا هستید، می‌تونید مستقیم سراغ بخش پایتونی بروید.
ویرگول داره میگه که برای خوندن این پست ۱۹ دقیقه زمان لازمه ولی واقعیت اینه که چون میزان کد خوبی داره پس این زمان درست نیست. حدسم اینه کمتر طول بکشه! ولی در هر صورت پست طولانیه‌ پس سفت بشینین که رفتیم. D:
https://www.slideshare.net/sauravmodak/a-beginners-guide-to-asynchronous-javascript-using-memes
https://www.slideshare.net/sauravmodak/a-beginners-guide-to-asynchronous-javascript-using-memes

مفاهیم

مفهوم Event loop

اگر بخوایم خیلی خیلی ساده توضیح‌ش بدیم اینطوریه که یک حلقه همیشه در حال اجرا رو فرض کنید، حالا داخل این حلقه به اتفاق‌های(Event) رخ داده واکنش نشون میدیم و اون‌هارو به دست مدیریت کننده‌های اتفاق می‌سپاریم(event dispatching). سوالی که الان براتون پیش اومده اینکه، منظور از اتفاق چیه؟ جوابم میشه خیلی چیزا(جواب جامع و شاملی بود D:)!! از کلیک کردن روی یک دکمه یا وارد کردن یک مقدار از طرف کاربر بگیر تا تموم شدن خواندن یک فایل. بزارین با یک مثال موضوع رو شفاف‌تر کنیم. یک وب سرور همیشه در حال اجراست که یک درخواست بهش می‌رسه(یک اتفاق رخ داد) درخواست رو می‌گیره و میده به کسی که باید اون رو مدیریت کنه. منتظر اتفاق بعدی می‌مونه. پاسخ به این درخواست حاضر میشه(دوباره یک اتفاق رخ داد) پاسخ رو میگیره و به کسی که باید جواب رو به کاربر برسونه میده(البته در این بین، وب سرورها مفاهیمی مثل صف و polling این جور چیزهارو هم دارند ولی خب هدف اینجا درک موضوع event loop هاست نه موشکافی نحوه کار وب سرورها. به نظرم یک سرچ درباره نحوه کار وب سرور داشته باشین واقعا مفیده).

اگر هم بخوایم برای event loop یک شبه کدی بنویسیم یک همچنین چیزی میشه:

while 1: wait for something to happen react to whatever happened

مفهوم coroutine

تعریف coroutine:
یک نوع تابع که امکان متوقف کردن(pause) اجرای خود قبل از تمام شدن بدنه‌ی تابع را دارد(یعنی قبل از رسیدن به انتهای تابع) و همچنین می‌تواند روند اجرا را به coroutine دیگری هدایت کند. به خاطر همین به courotineها cooperative multitasking هم گفته میشه.

بزارین یکم تعریف رو بهترش کنیم، ما وقتی یک تابع می‌نویسیم یعنی از خط اول شروع به اجرا کن تا آخرش و بعد از تابع بیا بیرون، قبول؟ حالا coroutine یک تابع هستش که وقتی در بخشی از بدنه خودش وقتی cpu باید منتظر بمونه اون رو آزاد می‌کنه یا به عبارت دیگه خودش رو متوقف می‌کنه(منظور از منتظر موندن cpu هم موارد مرتبط با I/O هستش. درمورد مسئله‌های I/O-bound و CPU-bound سرچ کنید). این برگشتن وسط تابع شاید شمارو یاد generatorها تو پایتون بندازه!

اگر با مسئله‌های I/O-bound و multithreading آشنا باشید می‌تونیم بگیم که، رفتار coroutine شبیه رفتار یک thread که تو multithreading هستش(برای فهم بهتر این مورد رو گفتم در عمل این دوتا خیلی با هم تفاوت دارند)!! با این تفاوت که، coroutine مورد اعتمادمونه و موقعی که cpu رو در اختیار دارد، کاری به کارش نداریم و در عوض اون هم موقعی که به cpu نیازی ندارد، آزادش می‌کنه. احتمالا الان دارین میگین که این حرفا یعنی چی؟ اعتماد چیه؟ مگه به بقیه اعتماد نداریم؟ و ... که البته حق هم دارید. بزارید با شباهت ماجرا شروع کنیم، تو پایتون multithreading اینطوریه که cpu هی از یک thread گرفته میشه و داده میشه به thread بعدی درسته(بحث GIL)؟ بشیه این اتفاق تو coroutine هم داریم یعنی cpu از یک coroutine داده میشه به coroutine دیگه. اما برسیم به تفاوت.

ببینید thread یک مشکلی داره و اونم اینکه وقتی cpu رو می‌گیره، ولش نمی‌کنه و اون رو تا زمانی که کارش تموم بشه در اختیار خودش نگه می‌داره، حتی اگر بین کارش یک زمانی‌هایی از cpu استفاده نکنه و منتظر یک سری موارد خارجی باشه، خیلی شیک میگه cpu فقط برای منه! عین یک بچه لجباز که همه چیز رو برای خودش می‌خواد! اما coroutine این طوری نیست و مثل یک بچه خوب وقتی کاراش رو انجام داد و دیگه نیازی به cpu نداشت، میگه بغلی بگیر، چیو بگیرم؟ cpu رو(حالا از این به بعد هر وقت بچه لجباز دیدین یاد thread و بچه خوب دیدین یاد coroutine نیوفتین و زندگی عادی رو داشته باشین D:)!

توجه کنید دو تا پاراگراف بالا coroutine و thread رو از لحاظ رفتاری با هم بررسی شدند. از لحاظ ساختار با هم خیلی متفاوت‌اند. به عنوان مثال ما تو thread یک چیزی به اسم call stack مجزا برای هر thread داریم در حالی که تو coroutine اصلا این طوری نیست(همیشه این یادتون باشه که coroutine یک فقط تابع‌ست).

thread and context switch

مشکل انحصار طلبی thread توسط سیستم‌عامل حل شده، یعنی سیستم‌عامل هر چند وقت یک بار میاد وسط و میگه بچه جون این cpu رو بده ببینیم، کار داریم باهاش(اصلا هم براش مهم نیست که کجای کار بوده و با بی‌رحمی تمام این کار رو انجام میده)! به این عمل گرفتن cpu و عوض کردن کار در حال اجرا context switch گفته میشه. طبیعتا این context switch هم هزینه‌هایی دارد(جلوتر میگیم).

https://i.redd.it/9tu18n684z331.jpg
https://i.redd.it/9tu18n684z331.jpg

اما شاید براتون سوال باشه که چرا context switch بَده؟ فرض کنید cpu در اختیار یک thread و داره کارش رو انجام میده و اتفاقا خیلی هم با شدت داره کار می‌کنه و زمان بی‌استفاده‌ای هم این وسط نداره. اما از اونجایی که سیستم‌عامل بهش اعتماد نداره، دقیقا وسط کارش میاد و cpu رو ازش می‌گیره و بعد تازه باید تصمیم بگیره ببینه نفر بعدی کیه و کی باید cpu رو در اختیار بگیره(حتی ممکنه پس از بررسی، cpu دوباره به همون نخ قبلی داده بشه). این context switch و گرفتن و دادن cpu یک سری هزینه‌هایی داره، به عنوان مثال باید یک داده‌هایی رو در جاهای مختلف مثل ثَبات‌ها(registery) و... بارگذاری(load) کنه و این موارد باعث افزایش زمان اجرا خواهند شد. طبیعتا هرچی تعداد این context switch بیشتر باشه این هزینه‌ها هم بیشتر میشه و این یعنی زمان اجرای یک برنامه بیشتر خواهد شد. دقیقا هم به خاطر همین موضوع context switch هستش که تو کارهای از جنس cpu bound(توجه کنید cpu-bound نه i/o-bound)، چند نخی(multi threading) زمان بیشتری رو نسبت به حالت یک نخی(single threading) خواهد داشت. چون cpu هی داره از این thread گرفته میشه و داده میشه به اون یکی و این تعویض کردن‌ها زمان اجرا رو بالا می‌بره در حالی که تو single thread این عملیات تعویض رو نداریم و کار پیش میره. از اون طرف کارهای I/O bound به دلیل اینکه cpu اکثرا منتظر ورودی هستش و بیکار نشسته، هزینه این context switch به چشم نمیاد هیچ، حتی باعث استفاده بهتر از cpu هم میشه. چون thread فعلی cpu رو بیکار نگه داشته و داره هدر میره، پس گرفتن cpu و دادن اون به thread که به cpu احتیاج داره به صرفه‌ست.

یک نکته‌ای که باید بهش توجه کرد اینکه coroutineهارو در عین حال که به عنوان یک بچه‌ی خوب میشناسیمشون، ولی اگر خوب تربیت نشده باشند، می‌تونند بچه خیلی بدی باشند! چطوری؟ به خاطر برنامه‌نویسی بد cpu رو آزاد نشه! این انحصار می‌تونه به شدت روی برنامه ما تاثیر بدی بزاره.

https://img.devrant.com/devrant/rant/r_1541402_ZzPjJ.jpg
https://img.devrant.com/devrant/rant/r_1541402_ZzPjJ.jpg

این بخش حسابی طولانی شد! بُدو بریم سراغ پایتون و ببنیم که این چیزهایی که گفتیم تو پایتون چطوریه؟

بخش پایتونی

بی هیچ حرف و حدیثی بیاین یک دونه coroutine بنویسیم:

>>> async def my_method(): ... print(&quotHi there, I'm a coroutine!&quot) >>> my_method() <coroutine object my_method at 0x7f3d46e94a40>

بله به همین سادگی! البته ما اجراش نکردیم(در واقع این فراخوانی منجر به اجرا نشد)!! ولی جلوتر نحوه اجرای اون رو هم خواهیم دید. در ضمن واقعیت اینکه ما تو coroutineها await هم داریم و همین await میگه که آقا جان cpu رو آزاد و خدمت شما. بزارین یک مثال ازش ببینیم:

>>> import asyncio >>> >>> async def my_method(): ... print(&quotHi there, I'm a real coroutine!&quot) ... await asyncio.sleep(5) ... print(&quotwith await in it.&quot) >>> my_method() <coroutine object my_method at 0x7f3d46e94a40>

نکته مهم:‌ قبل از اینکه ادامه بدیم بزارین یک نکته خیلی مهم رو خدمتتون عرض کنم(این مورد اکثرا اشتباه گرفته میشه) و اونم اینکه coroutine لزوما به معنای همزمانی(concurrency) نیست! بله تو همزمانی استفاده زیادی می‌تونه داشته باشه ولی نوشتن یک coroutine به معنای همزمانی نیست. جلوتر مثال‌هایی از این مورد رو می‌بینیم.

کتابخونه asyncio

این کتابخونه(asyncio) تو نسخه ۳٫۴(شایدم ۳٫۳ دقیق یادم نیست!!) رسما به cpython اضافه شد ولی یک سری مشکلات(خوانایی کم) باعث شد که تو نسخه‌های بعدی پایتون بروزرسانی‌هایی داشته باشه. به خاطر همین ممکنه تو نت یک سری کدهارو پیدا کنید که کار نکنه. پس حواستون به این موضوع باشه.
توصیه‌ای که دارم اینکه از نسخه ۳٫۸ به بالا استفاده کنید چون بعضی از چیزها تو ۳٫۸ از رده خارج شده و قراره تو نسخه های ۳٫۱۰ یا ۳٫۱۱ کلا از پایتون حذف شوند. مطالب این پست برای نسخه 3.7 و به بالا پایتون می‌باشد.

طبق مستندات asyncio یک کتابخونه برای نوشتن کدهای asynchronous با استفاده از سینتکس async/await هستش. از این کتابخونه در مواردی مثل فریم‌ورک‌های asynchronous استفاده میشه تا بتونیم چیزهایی مثل web-servers, database connection libraries, distributed task queues داشته باشیم.

بزارین با یک مثال ساده شروع کنیم:

# Python 3.7+ import asyncio async def main(): print('Hello ...') await asyncio.sleep(1) print('... World!') asyncio.run(main())

بله برای اجرای یک coroutine می‌تونیم از asyncio.run کمک بگیریم.

توجه کنید که asyncio.run همیشه یک event loop ایجاد میکنه و بعد از اتمام کار نیز اون رو می‌بنده. درضمن به صورت همزمان هم نمی‌تونیم از دوتا asyncio.run در یک thread استفاده کنیم و خطا می‌گیریم. به مثال زیر توجه کنید:

import asyncio async def test(): await asyncio.sleep(2) print(&quottest method&quot) async def main(): asyncio.run(test()) print(&quotmain method&quot) asyncio.run(main())

وقتی این کد را اجرا کنیم به همچین اروری برمی‌خوریم:

RuntimeError: asyncio.run() cannot be called from a running event loop

برای همین گفته میشه از این run برای entry point اصلی برنامه‌های async استفاده بشه و ایده‌آل هم اینه فقط یک بار فراخوانی بشه.

خب پس برای فراخوانی coroutineهای مختلف چیکار کنیم؟ به طور کلی برای اجرای یک coroutine سه روش وجود دارد:

  • asyncio.run
  • await
  • asyncio.create_task

توضیحات و نحوه استفاده asyncio.run رو بالاتر دیدیم اما برسیم به مورد دوم و ببینیم که تو عمل چطوره؟ تکه کد زیر مدنظر داشته باشید(به زمان‌ها هم توجه کنید):

import asyncio import time async def say_after(delay, what): await asyncio.sleep(delay) print(what) async def main(): print(&quot******************************&quot) print(f&quotstarted at {time.strftime('%X')}&quot) await say_after(5, 'hello') await say_after(2, 'world') print(f&quotfinished at {time.strftime('%X')}&quot) print(&quot******************************&quot) asyncio.run(main()) نتایج ****************************** started at 17:10:50 hello world finished at 17:10:57 ******************************

اگر حواستون به زمان‌ها باشه میبینین که کد کاملا به صورت ترتیبی اجرا شده و اصلا بحث همزمانی(concurrency) رو نداریم. دلیلش هم اینکه اگر coroutine با await اجرا بشه، منتظر تموم شدن اجرای اون coroutine می‌مونه و بعد میره خط بعد. به خاطر همین هم به جای ۵ ثانیه ۷ ثانیه طول کشید و به جای اینکه اول world چاپ بشه، hello رو اول می‌بینیم. پس می‌بینیم که coroutineها لزوما همیشه به صورت همزمان(concurrency) نیستند.

https://media.makeameme.org/created/this-is-confusing-ifq340.jpg
https://media.makeameme.org/created/this-is-confusing-ifq340.jpg

بریم سراغ مثال حالت سوم و ببینیم اون چطوریاست:

import asyncio import time async def say_after(delay, what): await asyncio.sleep(delay) print(what) async def main(): task1 = asyncio.create_task(say_after(5, 'hello')) task2 = asyncio.create_task(say_after(2, 'world')) print(f&quotstarted at {time.strftime('%X')}&quot) # Wait until both tasks are completed (should take around 2 seconds.) await task1 await task2 print(f&quotfinished at {time.strftime('%X')}&quot) نتایج ****************************** started at 17:20:30 world hello finished at 17:20:35 ******************************

خب الان اون نتیجه ای که می‌خواستیم رو گرفتیم. دلیلش هم اینکه در حالت سوم coroutine تحت چیزی به اسم Task اجرا میشه(coroutine با wrapper تحت عنوان task اجرا میشه). Task برای اجرای coroutineها به صورت زمانبندی شده هستند. در واقع وقتی create_task رو فراخوانی می‌کنیم داریم میگیم که این coroutine رو زمان‌بندی کن تا در سریع زمان ممکن اجرا بشه. در واقع task یک چیزی شبیه future هستش(جلوتر میگیم). Task برای اجرای حتما نیاز داره تا یک event loop وجود داشته باشه. یعنی تحت شرایط زیر به خطا می‌خوریم:

>>> import asyncio >>> async def test(a): ... await asyncio.sleep(2) ... print(&quottest&quot) >>> async def main(): ... await test(1) ... print(&quotmain&quot) >>> asyncio.create_task(main()) Traceback (most recent call last): File &quot<stdin>&quot, line 1, in <module> File &quot/usr/lib/python3.8/asyncio/tasks.py&quot, line 381, in create_task loop = events.get_running_loop() RuntimeError: no running event loop

راستی task یک مورد داره و اونم اینکه thread-safe نیست. task یکی از مواردیه که تو پایتون ۳٫۷ اضافه شد و قبل اون از asyncio.ensure_future استفاده می‌شد.

بزارین از خوبی‌های task چند مورد رو تیتروار اشاره کنیم(از اونجایی که پست طولانی میشه توضیح داده نمیشه):

  • امکان لغو یک task
  • امکان بررسی وضعیت یک task که آیا تموم شده یا لغو شده.
  • امکان دریافت نتیجه یا خطای حاصل
  • حذف یا اضافه کردن یک callback برای بعد از اتمام task

و...

اشیا awaitable

اگر ما بتونیم عبارت await رو برای یک شی استفاده کنیم در واقع اون شی یک awaitable هستش. توی پایتون ۳ نوع اصلی awaitable داریم:

  • coroutine
  • Task
  • Futures

که دو مورد اول رو بالاتر توضیح دادیم پس بریم سراغ Future و ببنیم چیه؟

میشه گفت که Future یک چیزی شبیه promise تو javascript هستش(احتمالا الان دارید میگین که خب promise چیه؟ D:). یا به عبارت دیگه یک شی‌ایه که نتیجه اجرای یک عملیات async رو بالاخره ارائه خواهد داد، که این نتیجه می‌تونه خطا هم باشه حتی! یا مثلا می‌تونید مثل یک صندوق پست در نظر بگیرید که اول کار وقتی نصب میشه خالیه ولی در آینده پر میشه و بسته پستی خواهد داشت.

از اونجایی که من تا حالا هیچ تجربه‌ای تو استفاده از Futures نداشتم و فقط خوندم درموردش، به خاطر همین برای این بخش به مستندات خود پایتون رجوع می‌کنم:

A Future is a special low-level awaitable object that represents an eventual result of an asynchronous operation. Not thread-safe.

When a Future object is awaited it means that the coroutine will wait until the Future is resolved in some other place.

Future objects in asyncio are needed to allow callback-based code to be used with async/await.

Normally there is no need to create Future objects at the application level code.

طبق گفته خود پایتون یک مثال خوب استفاده از Future این میتونه باشه:

import asyncio import concurrent.futures def blocking_io(): # File operations (such as logging) can block the # event loop: run them in a thread pool. with open('/dev/urandom', 'rb') as f: return f.read(100) def cpu_bound(): # CPU-bound operations will block the event loop: # in general it is preferable to run them in a # process pool. return sum(i * i for i in range(10 ** 7)) async def main(): loop = asyncio.get_running_loop() ## Options: # 1. Run in the default loop's executor: result = await loop.run_in_executor(None, blocking_io) print('default thread pool', result) # 2. Run in a custom thread pool: with concurrent.futures.ThreadPoolExecutor() as pool: result = await loop.run_in_executor(pool, blocking_io) print('custom thread pool', result) # 3. Run in a custom process pool: with concurrent.futures.ProcessPoolExecutor() as pool: result = await loop.run_in_executor(pool, cpu_bound) print('custom process pool', result) asyncio.run(main())

برای اینکه مطلب طولانی‌تر نشه کد رو توضیح نمیدم ولی اگر کد بالا رو کسی خواست تو کامنت‌ها بگه تا توضیح بدم.

خب بریم سراغ gather و wait یک چند تا مثال ازشون ببینیم و مطلب رو کم کم جمعش کنیم.

gather

ما از gather برای اجرای چندین awaitable به صورت همزمان(concurrent) در ترتیبی که داده شده، استفاده می‌کنیم. حالا اگر نوع این awaitable برابر با coroutine باشه به صورت خودکار به Task تبدیل میشه.

احتمالا یکم براتون گنگ و نامفهومه پس بریم سراغ مثال عملی:

import asyncio async def factorial(name, number): f = 1 for i in range(2, number + 1): print(f&quotTask {name}: Compute factorial({i})...&quot) await asyncio.sleep(1) f *= i print(f&quotTask {name}: factorial({number}) = {f}&quot) async def main(): # Schedule three calls *concurrently*: await asyncio.gather( factorial(&quotA&quot, 2), factorial(&quotB&quot, 3), factorial(&quotC&quot, 4), ) asyncio.run(main()) # Expected output: # Task A: Compute factorial(2)... # Task B: Compute factorial(3)... # Task C: Compute factorial(4)... # Task A: factorial(2) = 2 # Task B: Compute factorial(3)... # Task C: Compute factorial(3)... # Task B: factorial(3) = 6 # Task C: Compute factorial(4)... # Task C: factorial(4) = 24

استفاده از gather چندتا نکته داره ولی به نظرم بهتره اول با wait آشنا بشیم و بعد بریم سراغ اون‌ها(برای اینکه با گفتن تفاوتشون درک اون‌ها مملوس‌تر باشه).

wait

عملکردش شبیه به gather هستش. به عنوان ورودی یک iterable از awaitableهارو می‌گیره و اون‌هارو به صورت همزمان اجرا می‌کنه. شاید با خودتون بگید که خب چرا دوتا ویژگی شبیه هم‌ وجود داره؟ اینطوری نیست که gather و wait دقیقا عین هم باشند، نه طبیعتا تفاوت‌هایی که دارند و هر کدوم استفاده خودش رو، اما قبل از اینکه به این تفاوت‌ها برسیم بزارین یک مثال از wait و نحوه استفاده اون ببینیم:

# this line is for keep align left to right. همون تکه کد مثال قبل رو مدنظر داشته باشید. async def main(): await asyncio.wait( [ factorial(&quotA&quot, 2), factorial(&quotB&quot, 3), factorial(&quotC&quot, 4), ]) asyncio.run(main())
https://pics.me.me/when-you-want-to-write-async-code-in-javascript-async-34986517.png
https://pics.me.me/when-you-want-to-write-async-code-in-javascript-async-34986517.png

تفاوت‌های gather و wait

  • برای wait یک دونه iterable از awaitable هارو پاس میدیم ولی gather نه، تک تک awaitable هارو پاس داده می‌شوند.
  • ما تو gather نتیجه اجرا رو خواهیم داشت ولی تو wait یک set به این شکل done, pending خواهیم داشت. یعنی:
#gather async def main(): results = await asyncio.gather(aw1(&quotA&quot, 2), aw2(&quotB&quot, 3), aw3(&quotC&quot, 4)) #gather async def main(): done, pending = await asyncio.wait([aw1(&quotA&quot, 2), aw2(&quotB&quot, 3), aw3(&quotC&quot, 4)])
  • یکی از مهم‌ترین تفاوت این دوتا تو پارامتریه که wait می‌گیره. پارامتر return_when مشخص می‌کنه که wait نتیجه برگشتی رو کی بهمون بده. کلا هم سه تا حالت داره: FIRST_COMPLETE, FIRST_EXCEPTION, ALL_COMPLETE که به صورت پیشفرض با حالت ALL_COMPLETE کار می‌کنه. تو FIRST_COMPLETE اولین awaitable که تموم شد نتیجه برمیگرده تو FIRST_EXCEPTION اولین حطایی که رخ بده و ALL_COMPLETE هم منتظر میمونه تموم شه چه خطا داشته باشیم چه نداشته باشیم. در حالی که توی gather اگر خطایی پیش نیاد، منتظر اجرای همه awaitableها می‌مونیم.
  • توی gather اگر خطایی رخ بده بقیه awaitablها اجرا نمی‌شوند ولی توی wait نه. البته برای gather هم میتونیم پارامتر return_exceptions رو برابر True بگیرم تا خطاها‌رو هم به عنوان خروجی در نظر بگیره و ادامه روند رو داشته باشیم.

یعنی در حالت عادی اینطوری میشه:

import asyncio async def my_method(i): await asyncio.sleep(i) if i % 2 == 0: print(i, &quotok&quot) else: print(i, &quotcrashed!&quot) raise ValueError async def main(): coros = [my_method(i) for i in range(10)] await asyncio.gather(*coros) asyncio.run(main())

که نتیجه خروجی این میشه:

0 ok 1 crashed Traceback (most recent call last): File &quot<stdin>&quot, line 1, in <module> File &quot/usr/lib/python3.8/asyncio/runners.py&quot, line 43, in run return loop.run_until_complete(main) File &quot/usr/lib/python3.8/asyncio/base_events.py&quot, line 616, in run_until_complete return future.result() File &quot<stdin>&quot, line 3, in main File &quot<stdin>&quot, line 7, in my_method ValueError

حالا اگه ما بیایم به gather پارامتر return_exceptions رو با مقدار True پاس بدیم، همه اجرا می‌شوند و خروجی به شکل زیر میشه:

async def main(): coros = [my_method(i) for i in range(10)] await asyncio.gather(*coros, return_exceptions=True) 0 ok 1 crashed 2 ok 3 crashed 4 ok 5 crashed 6 ok 7 crashed 8 ok 9 crashed

اگر با wait فراخوانی رو انجام می‌دادیم همچین خروجی‌ای داشتیم:

async def main(): coros = [my_method(i) for i in range(10)] await asyncio.wait(coros) 0 ok 1 crashed 2 ok 3 crashed 4 ok 5 crashed 6 ok 7 crashed 8 ok 9 crashed Task exception was never retrieved future: <Task finished name='Task-67' coro=<my_method() done, defined at <stdin>:1> exception=ValueError()> Traceback (most recent call last): File &quot<stdin>&quot, line 7, in my_method ValueError Task exception was never retrieved future: <Task finished name='Task-66' coro=<my_method() done, defined at <stdin>:1> exception=ValueError()> Traceback (most recent call last): File &quot<stdin>&quot, line 7, in my_method ValueError Task exception was never retrieved future: <Task finished name='Task-68' coro=<my_method() done, defined at <stdin>:1> exception=ValueError()> Traceback (most recent call last): File &quot<stdin>&quot, line 7, in my_method ValueError Task exception was never retrieved future: <Task finished name='Task-72' coro=<my_method() done, defined at <stdin>:1> exception=ValueError()> Traceback (most recent call last): File &quot<stdin>&quot, line 7, in my_method ValueError Task exception was never retrieved future: <Task finished name='Task-69' coro=<my_method() done, defined at <stdin>:1> exception=ValueError()> Traceback (most recent call last): File &quot<stdin>&quot, line 7, in my_method ValueError
  • توی wait نتیجه همه‌ی awaitableها به عنوان done در نظر گرفته میشه حتی اگر exception رخ داده باشه و برای اینکه بتونیم خطای رخ داده رو بفهمیم یک همچین حرکتی می‌زنیم:
async def main(): coros = [my_method(i) for i in range(10)] done, pending = await asyncio.wait(coros) for task in done: try: await task except Exception as err: print(&quotooh crap we have exception!&quot, repr(err))
  • توی cancel کردن هم با هم تفاوت دارند ولی چون پست خیلی طولانی شده به این لینک اکتفا می‌کنم!

دیگه چون مطلب خیلی خیلی طولانی شده، بعضی چیزها درباره coroutine رو تیتروار مرور می‌کنیم و رد میشیم:

− می‌تونیم به کمک shield از کنسل شدن یک Task جلوگیری کنیم.

res = await shield(something())

− برای تنظیم محدودیت مدت زمان اجرا از timeout استفاده می‌کنیم.

async def eternity(): # Sleep for one hour await asyncio.sleep(3600) print('yay!') async def main(): # Wait for at most 1 second try: await asyncio.wait_for(eternity(), timeout=1.0) except asyncio.TimeoutError: print('timeout!') asyncio.run(main()) # Expected output: # timeout!

− با استفاده از to_thread می‌تونیم تابع خودمون رو توی یک thread جدید اجرا کنیم.

− به کمک run_coroutine_threadsafe میتونیم یک coroutine رو داخل event loop مدنظر خودمون اجرا کنیم.

− برای گرفتن Task در حال اجرا از current_task استفاده می‌کنیم.

− با all_tasks هم همه‌ی Taskهایی که هنوز تموم نشدند رو بدست میاریم.

به نظرم با coroutine و کتابخونه asyncio(البته بخش coroutine) تا حد خوبی آشنا شدیم. میدونم که میشد بعضی جاهارو بیشتر بسط داد ولی خب هدف بیشتر آشنا شدن با این موراد بود نه جزيیات خیلی زیاد و اگر کسی نیاز داشت یا کنجکاو بود قطعا می‌تونه با جستجو بیشتر به اهدافش برسه. :)

https://medium.com/hackernoon/has-the-python-gil-been-slain-9440d28fa93d
https://medium.com/hackernoon/has-the-python-gil-been-slain-9440d28fa93d

با یک مثال که ارسال چند درخواست http به صورت همزمان هستش، مطلب رو تموم می‌کنیم:

import asyncio from aiohttp import request async def fetch(url): async with request(&quotGET&quot, url) as r: return await r.text(&quotutf-8&quot) URLS = [&quothttps://python.org&quot, &quothttps://duckduckgo.com&quot] async def main(): coros = [fetch(url) for url in URLS] result = await asyncio.gather(*coros) # or you can use wait --> done, pending = await asyncio.wait(coros) for result in results: print(f&quot{result[:20]}&quot) if __name__ == &quot__main__&quot: asyncio.run(main())

همین! شاد و خندون باشید و لبخند لطفا :)

منابع:

  • https://docs.python.org/3/library/asyncio-task.html
  • https://youtu.be/GSiZkP7cI80 (ویدیو فاخریه ببینید)


coroutinepythonpython coroutineasyncioasync
یه وحید از نوع برنامه نویسش :)
شاید از این پست‌ها خوشتان بیاید