سیر تکامل Async: از کالبکها تا Async/Await
خلاصهٔ کاملتر
وقتی سرورها باید هزاران اتصال همزمان رو مدیریت کنن، رویکرد کلاسیک «یک Thread به ازای هر اتصال» خیلی زود به دیوار میخوره. هر Thread حافظه میخواد، زمان ایجادش چشمگیره، و مدیریت هزاران Thread با هم CPU رو درگیر کارهایی میکنه که هیچ ارزش واقعیای ندارن. دن کگل این مشکل رو در ۱۹۹۹ «C10K Problem» نامید و از اون موقع برنامهنویسها دنبال راهحلهای بهتری گشتن.
اولین موج، کالبکها بودن. ایده ساده بود: به جای اینکه Thread منتظر بمونه، یه تابع ثبت کن که وقتی عملیات I/O تموم شد اجرا بشه. Event Loopهایی مثل epoll و kqueue هزاران اتصال رو روی چند Thread مدیریت میکردن. Node.js و Nginx بر همین اساس ساخته شدن و موفقیت بزرگی هم داشتن.
ولی کالبکها جریان برنامه رو وارونه میکنن. به جای «اول A، بعد B، بعد C»، باید مینوشتی «A رو اجرا کن، وقتی تموم شد این تابع رو صدا بزن که B رو اجرا میکنه، بعد اون تابع دیگه رو که C رو اجرا میکنه». نتیجهاش شد اون چیزی که توی دنیای جاوااسکریپت بهش «Callback Hell» میگن. ضمناً خطاها طبیعی بالا نمیاومدن، چون Call Stack واقعی وجود نداشت، و هیچ مکانیزم عمومیای برای لغو عملیات در کار نبود.
موج دوم با Promiseها (جاوااسکریپت) و Futureها (جاوا، Rust) اومد. ایده این بود که عملیات async بلافاصله یه آبجکت برگردونه که نماینده «مقدار آیندهست». این رویکرد کدها رو خواناتر میکرد: به جای تودرتوی کالبک، زنجیرهای از .then() داشتی و یه .catch() واحد برای مدیریت خطا. اما Promiseها فقط یکبار resolve میشن و برای stream یا رویدادهای تکراری مناسب نیستن. ترکیبشون برای منطق پیچیدهتر دستوپاگیر میشه، و در جاوااسکریپت، یه Promise که بدون .catch() reject بشه، اول از همه به سکوت خطا رو میبلعید—مشکلی که Node.js مجبور شد با کرش کردن Process بهش جواب بده.
موج سوم، Async/Await بود که C# در ۲۰۱۲ پیشگامش شد و بعد جاوااسکریپت، پایتون، Rust، Kotlin و Swift هم اتخاذش کردن. این سینتکس کد async رو درست مثل کد ترتیبی معمولی مینوشت: متغیرها طبیعی bind میشدن، میشد از try/catch استفاده کرد، و حلقهها با await داخلشون کار میکردن. خوانایی بهشدت بهتر شد و صنعت با سرعت این رویکرد رو پذیرفت.
البته Async/Await هم بدون مشکل نیست. مشهورترین انتقاد، مسئله «رنگبندی توابع» (Function Coloring)ه: توابع async و sync نمیتونن بهراحتی باهم ترکیب بشن. وقتی یه تابع sync رو async میکنی، تمام فراخوانهایش هم باید تغییر کنن—و این تغییر مثل ویروس در کل کد پخش میشه. بعضی زبونها مثل Go با Goroutineها و Erlang با Actorها این مشکل رو بهکلی دور زدن.
نکته مهم اینه که هر موج یه مشکل واقعی رو حل کرد، اما بدون هزینه نبود. کالبکها کارایی آوردن اما کد رو به هم ریختن. Promiseها کد رو مرتبتر کردن اما مشکلات ترکیب و خطاهای خاموش آوردن. Async/Await خوانایی آورد اما مسئله رنگبندی رو تشدید کرد.
درک این تاریخچه به برنامهنویس کمک میکنه که انتخابهاش رو هوشمندانهتر بکنه—بدونه چرا ابزارها اینطوری طراحی شدن، کجا خوب کار میکنن، و کجا باید دنبال جایگزین بگرده.
نکات کلیدی:
- مشکل C10K نشون داد که مدل Thread-per-connection برای سرورهای پرترافیک مقیاسپذیر نیست
- کالبکها اولین راهحل بودن: کارایی بالا، اما کد پیچیده و مدیریت خطای سخت
- Promise و Future کد رو خواناتر کردن، اما برای stream و منطق پیچیده دستوپاگیرن
- Async/Await کد async رو مثل کد ترتیبی معمولی مینویسه و سریع جهانگیر شد
- مشکل «رنگبندی توابع» بزرگترین ایرادیه که Async/Await هنوز باهاش دستوپنجه نرم میکنه
- Go و Erlang با رویکردهای متفاوت (Goroutine و Actor) از این مشکل فرار کردن




