چرا async/await تو هر زبون فرق میکنه؟
خلاصهٔ کاملتر
ماجرا از یه سوال بهظاهر ساده شروع میشه: وقتی یه تابع async یه تسک رو تو پسزمینه spawn میکنه و منتظرش نمیمونه - یعنی fire-and-forget - اون تسک دقیقاً کِی و چطور تموم میشه؟ محققهای دانشگاه براون این برنامهی کوچیکو رو هفت تا رانتایم واقعی اجرا کردن: Asyncio و Trio تو پایتون، C#، جاوااسکریپت، Tokio و Smol تو راست، و سوئیفت.
async fn write_to_log():
print("A")
await sleep(2)
print("B")
async fn fire_and_forget():
task = spawn write_to_log()نتیجه؟ چهار جواب متفاوت، برای یه برنامه که کل کارش نوشتن یه خط لاگه. تو مقالهای که از این کار نوشتن - «کاوش فضای طراحی async/await» - نویسندهها میگن حتی برای سه تا نسخهی متفاوت از همین برنامه، هیچ دو تا رانتایمی جواب یکسان نمیدن.
به گفتهی نویسندهها، این اختلاف از ۹ تا «بُعد طراحی» جدا میاد که رو سه دسته تقسیم میشن: شروع عمر تسک، پایان عمرش، و لغوش. مثلاً بُعد Eagerness (یعنی اینکه یه تسک همون لحظهی spawn شروع به اجرا بشه یا بمونه تا اولین await) رو هر زبون یهجور تصمیم گرفته. دو تا بُعد دیگه هم مستقیم رو مثال بالا اثر میذارن: Extent یعنی تسک میتونه از تابعی که توش ساخته شده عمر بیشتری داشته باشه یا نه، و Destruction یعنی وقتی اون تابع تموم میشه، تسک باقیمونده لغو میشه یا صبر میکنن تموم بشه.
سوئیفت و پایتون+Trio، برخلاف بقیه، Extent رو محدود به تابع میدونن، یعنی تسک نمیتونه از fire_and_forget عمر بیشتری داشته باشه. ولی تو Destruction فرق میکنن: سوئیفت تسکو لغو میکنه (برای همین فقط «AC» چاپ میشه)، در حالی که Trio مودبانه صبر میکنه تسک تموم بشه (برای همین «ABC» چاپ میشه). نویسندهها برای اینکه این رفتارها رو دقیق توضیح بدن، یه مدل صوری هم ساختن که مرحلهبهمرحله نشون میده هر رانتایم رو کدوم شاخه از تصمیمها میره.
نکات کلیدی:
- یه برنامهی سادهی fire-and-forget رو ۷ تا رانتایم مختلف اجرا شد و ۴ جواب متفاوت داد
- مقالهی «کاوش فضای طراحی async/await» ۹ بُعد طراحی رو تو ۳ دسته شناسایی کرده
- سوئیفت و Trio هر دو Extent محدود به تابع دارن ولی سوئیفت تسکو لغو میکنه، Trio صبر میکنه
- خروجی سوئیفت «AC»ـه، خروجی Trio «ABC»ـه
- نویسندهها یه مدل صوری هم ساختن تا اجرای هر رانتایم رو مرحلهبهمرحله توضیح بدن




