چهار شهر، هرکدام با یک درخواست که زمان متفاوتی میبرد. دو راه برای گرفتن جوابها، و دو ترتیب متفاوت:
list(ex.map(bekesh, shahrha))میدهد['tehran', 'shiraz', 'mashhad', 'tabriz'][f.result() for f in as_completed(fs)]میدهد['shiraz', 'tabriz', 'mashhad', 'tehran']
هر دو همان چهار کار را همزمان اجرا کردند و هر دو در یک مدت تمام شدند. map ترتیب ورودی را نگه میدارد و برای همین منتظر تهران میماند تا بتواند اولین قلم را تحویل دهد. as_completed هرچه زودتر تمام شد را همان لحظه میدهد، و شیراز که پنجاه میلیثانیه گرفت اول از همه رسید. بیشتر باگهای همروندی که در کد واقعی دیده میشوند نه مسابقه بر سر دادهاند و نه قفل کم دارند: کدی هستند که فرض کرده جوابها به همان ترتیبی برمیگردند که کارها فرستاده شدهاند.
رشته اجرا کار پردازنده را سریع نمیکند
این را میشود اندازه گرفت و جوابش برای خیلیها غافلگیرکننده است. چهار محاسبه سنگین یکسان، روی یک مکبوک با تراشه M1 Max و پایتون 3.14 در تاریخ ۲۰۲۶-۰۹-۰۳:
- پشت سر هم، بدون هیچ همروندی: ۱٫۰۱ ثانیه
- با چهار رشته اجرا: ۱٫۰۴ ثانیه
- با چهار فرایند جدا: ۰٫۴۱ ثانیه
رشتهها نهتنها کمکی نکردند، کمی هم بدتر شدند. دلیلش قفل سراسری مفسر است: در یک فرایند پایتون، هر لحظه فقط یک رشته اجرا بایتکد اجرا میکند. فرایندها هرکدام مفسر خودشان را دارند و برای همین واقعاً روی چهار هسته پخش میشوند، به قیمت اینکه داده باید میانشان کپی شود.
همان آزمون با کاری که فقط منتظر میماند وارونه جواب میدهد: پشت سر هم ۰٫۶۸ ثانیه، با چهار رشته اجرا ۰٫۳۰ ثانیه. رشتهای که منتظر شبکه است قفل را رها میکند، و بقیه در همان مدت کار میکنند. قاعده کوتاه است: منتظر ماندن را با رشته اجرا موازی کنید، حساب کردن را با فرایند.
هر جایی که انتظار هست، جای سوئیچ هم هست
وقتی دو رشته یک شمارنده مشترک را عوض میکنند، خواندن و نوشتن دو کار جدا هستند و بین آن دو ممکن است رشته دیگری وسط بیفتد. نتیجهاش این است که دو کاهش انجام میشود و فقط یکی ثبت میشود. راهحل یک قفل دور همان دو سطر است، نه دور کل تابع، و بهایش این است که آن ناحیه دیگر موازی نیست. سادهترین راه فرار هم این است که حالت مشترکی وجود نداشته باشد: هر کار نتیجه خودش را برگرداند و جمع کردن در انتها و در یک رشته انجام شود.
جایی که کار گیر میکند، انتخاب اشتباه ابزار روی سرور کوچک است
بیشتر اسکریپتهایی که یک تیم در ایران مینویسد وقتشان را پای شبکه میگذرانند، چون خود درخواستها از مسیرهای کند و باواسطه رد میشوند. این دقیقاً همان حالتی است که رشته اجرا در آن کمک میکند و یک تغییر سه سطری است. اتفاقی که در عمل میافتد این است که آموزشها کلمه سرعت را میبینند و سراغ فرایند میروند؛ روی یک سرور مجازی تکهستهای، که رایجترین چیزی است که اجاره میشود، چهار فرایند نهتنها چیزی سریعتر نمیکند بلکه چهار مفسر جدا بالا میآورد و حافظه را تمام میکند. اول اندازه بگیرید که برنامهتان منتظر است یا مشغول؛ کل انتخاب همین یک جواب است.
ابزار، و سالی که این حرف درست است
در سال ۲۰۲۶ نقطه شروع concurrent.futures است که در کتابخانه استاندارد و رایگان است. جای ساختن دستی فهرستی از threading.Thread و صدا زدن join روی تکتکشان را گرفته، و مهمتر اینکه خطای داخل یک کار را نگه میدارد و موقع result() بالا میآورد؛ در روش دستی، خطا در رشته اجرا چاپ میشد و برنامه اصلی هیچوقت نمیفهمید. برای عوض کردن رشته اجرا با فرایند فقط نام کلاس استخر عوض میشود و بقیه کد دست نمیخورد، که همین یک خط تفاوت ارزش این کتابخانه است.
پرسش بعدی این است که وقتی کاری منتظر است، چطور میشود بدون رشته اجرا هم منتظر ماند، که همان مدل تکرشتهای است و در کالبک، پرامیس، و انتظار روی نتیجه از نزدیک دیده میشود. و اگر میخواهید بدانید همین ترتیب نامعین در سمت سرور چه شکلی است، درخواست میآید، پاسخ میرود، و هرچه میان این دو است جایی است که هر درخواست یک کار همزمان دیگر است. پیدا کردن این باگها هم بدون چه چیزی ثبت شود تا خرابی بعدی قابل تشخیص باشد تقریباً ناممکن است، چون با اجرای دوباره ناپدید میشوند.