همزمانی (Concurrency) یعنی برنامه بتواند چند کار را همزمان پیش ببرد و روی پردازندهی چندهستهای آنها را واقعاً موازی اجرا کند. مدل همزمانی در زبان برنامهنویسی سلام بر پایهی نخهای سیستمعامل است: نخساز و نخپیوند نخ را شروع میکنند و منتظرش میمانند، کلیدواژهی برو یک فراخوانی را روی استخر وظیفهی مشترک و محدود اجرا میکند، کانالها مقدار را بین نخها جابهجا میکنند و کتابخانههای همگام سازی و اتمی از دادهی مشترک محافظت میکنند. زبالهروبی (Garbage Collector) در کار نیست که نخها را متوقف کند و async/await هم وجود ندارد.
این مقاله هر ابزار را با برنامهای که میتوانید اجرا کنید نشان میدهد، میگوید کدام ابزار به کار کدام مسئله میآید، این مدل را با Go، Rust و جاوا مقایسه میکند و اشتباههایی را فهرست میکند که به قفلشدن یا از کار افتادن برنامه میانجامند. درس کار همزمان در دورهی آموزشی مقدمهی آرامتری است؛ این نوشته مروری است برای کسانی که پیشتر در زبان دیگری کد همزمان نوشتهاند.
مدل همزمانی زبان سلام: نخ سیستمعامل، استخر و بدون async/await
هر واحد همزمان در سلام در نهایت یک نخ (Thread) سیستمعامل است. کتابخانهی استاندارد روی همین پایه ابزارهای سطحبالای آشنا را ساخته است:
| نیاز | ابزار |
|---|---|
| شروع یک نخ و انتظار برای آن | نخساز و نخپیوند (درونی زبان)، و نخ برای گروهها و سقف تعداد |
| کارهای کوچک و زیاد، با سقف مشخص | برو تابع(ع)، استخر.جدید و استخر.ارسال از استخر |
| یک حلقه روی اندیسها با همهی هستهها | استخر.برای موازی و استخر.نگاشت موازی |
| نتیجه و خطای وظیفهها | استخر.ناهمزمان و منتظر ماندن، استخر.گروه کار جدید و گروه انتظار |
| فرستادن مقدار بین نخها | کانال: کانال بافردار یا ملاقاتی، گزینش، زمانسنجها |
| محافظت از دادهی مشترک | همگام سازی: قفل، قفل خواندن نوشتن، متغیر شرط، گروه انتظار، نشانگر، یک بار |
| یک عدد یا پرچم، بدون قفل | اتمی: صحیح۶۴ و پرچم |
| لغو کار و تعیین مهلت | زمینه: با لغو، با مهلت، با موعد |
نام انگلیسی هر کدام از اینها (pool.New، sync.Mutex، chan.Select و دیگران) در برنامهای که به انگلیسی نوشته شده همان کار را میکند؛ کتابخانه یکی است و فقط املا فرق دارد.
انتخاب نخ واقعی پیامدهایی دارد که بهتر است از همان اول بدانید. وظیفههای روی استخر نخ واقعیاند، نه نخ سبز (Green Thread): وظیفهای که منتظر چیزی میماند، کارگرش را هم مشغول نگه میدارد. استدلال دربارهی این مدل از یک زماناجرای ناهمگام (async runtime) سادهتر است، و کتابخانههای C که از راه FFI صدا زده میشوند دقیقاً همان رفتاری را دارند که در خود C دارند. از طرف دیگر، سروری با ده هزار اتصال بیکار نمیخواهد ده هزار نخ داشته باشد؛ به همین دلیل سرور HTTP سلام روی لینوکس به جای یک نخ برای هر اتصال، از یک استخر کارگر مبتنی بر epoll استفاده میکند.
نخها با نخساز و نخپیوند
نخساز(تابع) تابع را روی یک نخ تازه شروع میکند و یک شناسه برمیگرداند؛ نخپیوند(شناسه) منتظر میماند تا آن نخ تمام شود. تابع نخ یا ورودی ندارد یا دقیقاً یک اشارهگر میگیرد: نخساز(تابع، داده). همین اشارهگر راهی است که هر نخ سهم خودش از کار را بگیرد و نتیجهاش را همانجا بگذارد. داده باید تا پایان کار نخ زنده بماند، پس در هیپ ساخته میشود.
واردسازی حافظه
ساختار بازه:
همگانی آغاز: صحیح۶۴ = ۰
همگانی انتها: صحیح۶۴ = ۰
همگانی مجموع: صحیح۶۴ = ۰
پایان
تابع جمع مربعها(ب: بازه*):
ناپایا جمع := ۰ برگردان صحیح۶۴
تکرار ب[۰].آغاز تا ب[۰].انتها در ای:
جمع += ای * ای
پایان
ب[۰].مجموع = جمع
پایان
تابع بازهی تازه(اول: صحیح۶۴، آخر: صحیح۶۴): بازه*:
ب := حافظه.تخصیص(اندازهگونه(بازه)) برگردان بازه*
ب[۰] = بازه { آغاز = اول، انتها = آخر }
برگشت ب
پایان
تابع ریشه:
پایین := بازهی تازه(۱، ۵۰۰۰۰۰)
بالا := بازهی تازه(۵۰۰۰۰۱، ۱۰۰۰۰۰۰)
ن۱ := نخساز(جمع مربعها، پایین)
ن۲ := نخساز(جمع مربعها، بالا)
نخپیوند(ن۱)
نخپیوند(ن۲)
سرچاپ "نیمهی اول:"، پایین[۰].مجموع
سرچاپ "نیمهی دوم:"، بالا[۰].مجموع
سرچاپ "جمع کل:"، پایین[۰].مجموع + بالا[۰].مجموع
حافظه.آزادسازی(پایین برگردان تهی*)
حافظه.آزادسازی(بالا برگردان تهی*)
پایاناین دو نخ هرگز به حافظهی مشترکی دست نمیزنند، پس به هیچ هماهنگیای نیاز ندارند؛ این سادهترین نوع برنامهی همزمان برای درست نوشتن است. هر نخ را دقیقاً یک بار پیوند بزنید و نتیجهاش را فقط پس از پیوند بخوانید؛ نخپیوند دوم روی همان شناسه از حافظهی آزادشده استفاده میکند. نخ.تعیین سقف(ن) تعداد نخهایی را که همزمان میتوانند زنده باشند محدود میکند و پس از رسیدن به سقف، نخساز به جای تمام کردن منابع سیستم، صفر برمیگرداند. نخ.اجرا با پشته هم نخی با پشتهی بزرگتر برای بازگشتهای عمیق میسازد.
کلیدواژهی برو و دادهی مشترک با قفل
برای کارهای کوچک و زیاد، ساختن یک نخ برای هر کار پرهزینه است و اگر سقفی در کار نباشد، خطرناک هم هست. نوشتن برو پیش از یک فراخوانی، آن را بهصورت وظیفه روی یک استخر مشترک اجرا میکند که برای هر هسته یک کارگر و صفی محدود دارد. وقتی صف پر است، برو صبر میکند، پس یک حلقهی تند نمیتواند کار بیپایان روی هم تلنبار کند. استخر.انتظار برو() منتظر همهی وظیفهها میماند. آرگومانها همان لحظهای حساب میشوند که خط برو اجرا میشود، پس برو ثبت(کد) درون حلقه مقدار همان دور را میفرستد.
وقتی وظیفهها یک داده را تغییر میدهند، به قفل نیاز دارند. در برنامهی زیر ۳٬۰۰۰ وظیفه کدهای وضعیت HTTP را در یک نگاشت مشترک میشمارند و یک قفل (Mutex) از آن محافظت میکند:
واردسازی استخر
واردسازی همگام سازی
ناپایا قفل := همگام سازی.قفل جدید()
ناپایا شمار وضعیت := نگاشت {} برگردان نگاشت<صحیح، صحیح>
تابع ثبت(کد: صحیح):
همگام سازی.قفل کردن(قفل)
شمار وضعیت.درج(کد، شمار وضعیت.دریافت با پیش فرض(کد، ۰) + ۱)
همگام سازی.باز کردن قفل(قفل)
پایان
تابع ریشه:
تکرار ۳۰۰۰ در ای:
ناپایا کد := ۲۰۰
اگر ای % ۱۰ == ۰:
کد = ۴۰۴
پایان
اگر ای % ۱۰۰ == ۰:
کد = ۵۰۰
پایان
برو ثبت(کد)
پایان
استخر.انتظار برو()
سرچاپ "۲۰۰:"، شمار وضعیت.دریافت با پیش فرض(۲۰۰، ۰)
سرچاپ "۴۰۴:"، شمار وضعیت.دریافت با پیش فرض(۴۰۴، ۰)
سرچاپ "۵۰۰:"، شمار وضعیت.دریافت با پیش فرض(۵۰۰، ۰)
همگام سازی.نابودسازی(قفل)
شمار وضعیت.آزادکن()
پایانبدون قفل کردن و باز کردن قفل، دو وظیفه ممکن است هر دو همان شمارش قدیمی را بخوانند و هر دو یکی به آن اضافه کنند، و دو درج همزمان حتی میتواند خود نگاشت را خراب کند. برای یک شمارندهی تنها، اتمی.افزودن روی یک اتمی.صحیح۶۴ خواندن، افزودن و نوشتن را در یک گام تجزیهناپذیر انجام میدهد و از قفل سبکتر است. کتابخانهی اتمی همهجا بدون قفل کار میکند، جز وقتی برنامه با tcc ساخته شود؛ آنجا برای هر خانه یک قفل با همان معنا به کار میرود. برو کلیدواژهای وابسته به جایگاه است، پس متغیری به نام برو همچنان کار میکند؛ در برنامههای انگلیسی همین کلیدواژه go نوشته میشود.
حلقهی موازی در سلام: برای موازی و نگاشت موازی
خیلی وقتها تنها چیزی که میخواهید این است که بدنهی یک حلقه را روی اندیسهای زیاد و میان همهی هستهها پخش کنید. استخر.برای موازی(ن، کارگرها، بدنه) بدنه را برای هر ای از ۰ تا ن - ۱ صدا میزند و وقتی همه تمام شدند برمیگردد؛ استخر.نگاشت موازی همین کار را میکند و نتیجهی هر فراخوانی را در یک وکتور<صحیح۶۴> جمع میکند. برنامهی زیر عددهای اول کمتر از یک میلیون را در ده بلوک میشمارد:
واردسازی استخر
تابع اول است(ن: صحیح۶۴): منطقی:
اگر ن < ۲: برگشت نادرست پایان
ناپایا مق := ۲ برگردان صحیح۶۴
تا مق * مق <= ن:
اگر ن % مق == ۰: برگشت نادرست پایان
مق++
پایان
برگشت درست
پایان
تابع شمار اولها(بلوک: صحیح۶۴): صحیح۶۴:
ناپایا یافته := ۰ برگردان صحیح۶۴
آغاز := بلوک * ۱۰۰۰۰۰
تکرار آغاز تا آغاز + ۹۹۹۹۹ در ن:
اگر اول است(ن): یافته++ پایان
پایان
برگشت یافته
پایان
تابع ریشه:
شمارها := استخر.نگاشت موازی(۱۰، استخر.تعداد پردازنده()، (ب: صحیح۶۴): برگشت شمار اولها(ب) پایان)
ناپایا جمع := ۰ برگردان صحیح۶۴
تکرار شمارها.طول() در ای:
جمع += شمارها.بگیر(ای)
پایان
سرچاپ "عددهای اول زیر یک میلیون:"، جمع
سرچاپ "در بلوک اول:"، شمارها.بگیر(۰)، "در بلوک آخر:"، شمارها.بگیر(۹)
شمارها.آزادکن()
پایانهر نتیجه در خانهی خودش مینشیند، پس خروجی به ترتیب تمام شدن بلوکها بستگی ندارد. صفحهی مرجع استخر قاعدهها را میگوید: هرگز به ترتیب نوبتها تکیه نکنید، هر نوبت را آنقدر بزرگ بگیرید که ارزش یک نخ را داشته باشد (تکههای بزرگ، نه تکتک عضوها)، و از درون بدنهی حلقه چاپ نکنید. به تا مق * مق <= ن: در تابع اول است دقت کنید: تا در سلام حلقه را تا وقتی شرط درست است ادامه میدهد. در فارسی این معنا طبیعی است، اما معادل انگلیسی آن، until، برنامهنویسان Ruby را غافلگیر میکند، چون در Ruby برعکس رفتار میکند.
گروه خطا و آینده (Future)
دو الگوی آشنا، errgroup در Go و Future در جاوا، در خود کتابخانهی استخر وجود دارند. یک گروه وظیفهها را با سقفی برای تعداد اجرای همزمان اجرا میکند و نخستین خطا را، به شکل یک رشتهی ناتهی، نگه میدارد. ناهمزمان یک وظیفه را روی استخر اجرا میکند و یک آینده (Future) برمیگرداند که با منتظر ماندن نتیجهاش را میگیرید:
واردسازی استخر
تابع بررسی(ردیف: صحیح): رشته:
اگر ردیف == ۱۳:
برگشت "ردیف ۱۳: قیمت منفی است"
پایان
برگشت ""
پایان
تابع جمع کند(ن: صحیح۶۴): صحیح۶۴:
ناپایا ج := ۰ برگردان صحیح۶۴
تکرار ن در ای:
ج += ای
پایان
برگشت ج
پایان
تابع ریشه:
گروه := استخر.گروه کار جدید(۴)
تکرار ۵۰ در ردیف:
استخر.گروه برو(گروه، (): برگشت بررسی(ردیف) پایان)
پایان
سرچاپ "نخستین خطا:"، استخر.گروه انتظار(گروه)
استخر.گروه توقف(گروه)
است := استخر.جدید(۲، ۸)
الف := استخر.ناهمزمان(است، (): برگشت جمع کند(۱۰۰۰۰۰۰) پایان)
ب := استخر.ناهمزمان(است، (): برگشت جمع کند(۲۰۰۰) پایان)
سرچاپ "الف ="، استخر.منتظر ماندن(الف)، "ب ="، استخر.منتظر ماندن(ب)
استخر.رها کردن(الف)
استخر.رها کردن(ب)
استخر.توقف(است)
پایاناستخر.جدید(۲، ۸) استخری با دو کارگر و جا برای هشت وظیفهی منتظر میسازد و تعداد نخهایش هرگز بیشتر نمیشود. استخر.جدید کشسان(کمینه، بیشینه، صف) وقتی همهی کارگرها مشغولاند تا بیشینه کارگر اضافه میکند و برای وظیفههایی که ممکن است منتظر یکدیگر بمانند انتخاب درستی است. چون وظیفهها نخاند، یک وظیفه روی استخر ثابتِ پر نباید هرگز منتظر وظیفهای بماند که پشت سرش در صف است؛ استخر.ارسال با اجرای مستقیم وقتی صف پر است وظیفه را روی همان نخ فراخواننده اجرا میکند و برای وظیفههایی که خودشان وظیفه میفرستند امن است.
کانال و گزینش در زبان سلام
کانالِ کتابخانهی کانال مقدارها را از یک نخ به نخ دیگر میبرد، و نخها جز خود کانال هیچ چیز مشترکی ندارند. مقداردهی(ن) به کانال جای ن مقدار میدهد؛ مقداردهی(۰) کانالی ملاقاتی (rendezvous) میسازد که در آن فرستادن صبر میکند تا گیرندهای مقدار را بردارد. پس از بستن() دیگر چیزی فرستاده نمیشود، و وقتی مقدارهای باقیمانده برداشته شدند، دریافت ورودی باز را نادرست میکند؛ این برای کارگر نشانهی توقف است.
واردسازی کانال
ناپایا کارها := کانال.کانال {} برگردان کانال.کانال<صحیح۶۴>
ناپایا نتیجهها := کانال.کانال {} برگردان کانال.کانال<صحیح۶۴>
تابع کارگر():
ناپایا باز := درست
تا درست:
ن := کارها.دریافت(باز)
اگر وارونه باز:
بشکن
پایان
نتیجهها.فرستادن(ن * ن)
پایان
پایان
تابع ریشه:
کارها.مقداردهی(۸)
نتیجهها.مقداردهی(۱۰۰)
ناپایا کارگرها := وکتور {} برگردان وکتور<صحیح۶۴>
دیرکن کارگرها.آزادکن()
تکرار ۳:
کارگرها.بیفزا(نخساز(کارگر))
پایان
تکرار ۱ تا ۱۰۰ در ای:
کارها.فرستادن(ای برگردان صحیح۶۴)
پایان
کارها.بستن()
هر ک در کارگرها:
نخپیوند(ک)
پایان
نتیجهها.بستن()
همه := کانال.تخلیه(نتیجهها)
ناپایا جمع := ۰ برگردان صحیح۶۴
هر م در همه:
جمع += م
پایان
سرچاپ "تعداد نتیجهها:"، همه.طول()، "جمع:"، جمع
همه.آزادسازی()
کارها.آزادسازی()
نتیجهها.آزادسازی()
پایانکانال.گزینش همزمان منتظر چند کانال میماند و شمارهی کانالی را برمیگرداند که مقدار داده است؛ اگر مهلت تمام شود -1 و اگر همهی کانالها بسته باشند -2 برمیگرداند. همراه با کانال.پس از و کانال.ضربان که پس از چند میلیثانیه یا در فاصلههای منظم مقدار تحویل میدهند، الگوهای مهلت و ضربان قلب (heartbeat) را که برنامهنویسان Go با select مینویسند پوشش میدهد:
واردسازی کانال
واردسازی استخر
تابع ریشه:
ناپایا تند := کانال.جدید(۱) برگردان کانال.کانال<صحیح۶۴>
ناپایا کند := کانال.جدید(۱) برگردان کانال.کانال<صحیح۶۴>
ناپایا دوکانال := وکتور {} برگردان وکتور<کانال.کانال<صحیح۶۴>>
دیرکن دوکانال.آزادکن()
دوکانال.بیفزا(تند)
دوکانال.بیفزا(کند)
است := استخر.جدید(۲، ۴)
استخر.ارسال(است، (): تند.فرستادن(۱ برگردان صحیح۶۴) پایان)
ناپایا م := ۰ برگردان صحیح۶۴
کدام := کانال.گزینش(دوکانال، م، ۱۰۰۰)
سرچاپ "کانال"، کدام، "پاسخ داد با مقدار"، م
کدام۲ := کانال.گزینش(دوکانال، م، ۵۰)
سرچاپ "گزینش دوم:"، کدام۲
تند.بستن()
کند.بستن()
سرچاپ "پس از بستن:"، کانال.گزینش(دوکانال، م، ۵۰)
استخر.توقف(است)
تند.آزادسازی()
کند.آزادسازی()
پایانبرخلاف select در Go، این یک تابع کتابخانهای روی کانالهایی با یک گونهی عضو است، نه دستوری روی هر عملیات دلخواه کانال. گزینش فرستادن، کوشش برای گزینش و کوشش برای گزینش فرستادن حالتهای فرستادن و غیرمسدودکننده را پوشش میدهند. کانال یک وکتور یا اشارهگر را بهصورت دستگیره کپی میکند، پس هر دو نخ به همان چیزی دسترسی دارند که دستگیره به آن اشاره میکند.
لغو، مهلت و دیگر ابزارهای همگامسازی
بقیهی جعبهابزار از طراحیهای آشنا پیروی میکند:
- زمینه با
با لغو،با مهلتوبا موعددرختی از زمینهها میسازد. لغو یک گره همهی گرههای زیرش را تمام میکند و موعدِ فرزند فقط میتواند موعدی را که به ارث برده کوتاهتر کند. کارگرهازمینه.تمام شدهرا بررسی میکنند یا درزمینه.انتظارمیمانند. همگام سازی.قفل خواندن نوشتنبرای خوانندههای زیاد و نویسندهای گاهبهگاه (با اولویت نویسنده)،همگام سازی.متغیر شرطبرای خوابیدن تا برقرار شدن یک شرط،همگام سازی.نشانگر(Semaphore) برای راه دادن حداکثر N نخ به یک بخش، وهمگام سازی.یک باربرای آمادهسازیای که باید دقیقاً یک بار اجرا شود.اتمی.پرچمباآزمون سپس تنظیمبرای نشانهی توقفی یکباره که دقیقاً یک فراخواننده برندهاش میشود.- رویدادها برای یک پخشکنندهی رویداد درونفرایندی با نخ کارگر اختیاری، و کرون برای کارهای زمانبندیشده که هر کدام روی نخ خودش اجرا میشود.
ابزارهای همگام سازی به جای سرکشی پیاپی (polling) روی یک متغیر شرط منتظر میمانند، پس نخی که منتظر است هیچ زمان پردازندهای مصرف نمیکند.
مقایسهی همزمانی در سلام با Go، Rust و جاوا
| سلام | Go | Rust | جاوا | |
|---|---|---|---|---|
| واحد همزمانی | نخ سیستمعامل، استخرهای محدود | گوروتین روی یک زمانبند | نخ سیستمعامل، یا وظیفهی async با زماناجرایی مثل Tokio | نخ سیستمعامل، و از جاوا ۲۱ نخ مجازی |
| نحو شروع | برو تابع(ع)، نخساز(تابع) | go f(x) | thread::spawn، tokio::spawn | اجراکنندهها (executor)، Thread.ofVirtual() |
| کانال | کتابخانه، با تابع گزینش | درونی زبان، دستور select | کتابخانه (std::sync::mpsc، crossbeam) | BlockingQueue و مشابههایش |
| async/await | ندارد | ندارد | دارد | ندارد (به جایش نخ مجازی) |
| جلوگیری از مسابقهی داده | انضباط خود برنامهنویس | تشخیصدهندهی مسابقه در زمان اجرا | در زمان کامپایل (Send، Sync) | انضباط خود برنامهنویس |
| زبالهروب | ندارد | دارد | ندارد | دارد |
خوانش منصفانهی این جدول: کلیدواژهی برو (و go در سلام انگلیسی) نحوش را از Go گرفته، اما زماناجرایش را نه. یک گوروتین چند کیلوبایت حافظه میگیرد و Go میتواند صدها هزار گوروتین را اجرا کند و گوروتینهای منتظر را ارزان کنار بگذارد؛ وظیفهی سلام هم در زمان اجرا و هم در زمان انتظار یک نخ سیستمعامل را اشغال میکند. برای کار موازیِ محاسباتی این تفاوت اهمیت چندانی ندارد و نبودِ زبالهروب مکثهای جمعآوری زباله را کاملاً حذف میکند. اما برای صدها هزار اتصال که بیشترشان بیکارند، مدل Go، یا یک حلقهی رویداد مانند همانی که درون سرور HTTP خود سلام کار میکند، انتخاب مناسبتری است.
شکاف بزرگتر، ایمنی است. Rust در زمان کامپایل ثابت میکند که دادهی مشترک میان نخها محافظت شده است؛ سلام چنین کاری نمیکند و Go و جاوا هم نمیکنند. جا انداختن یک قفل در سلام باگی بیصداست. مقالهی سلام در برابر Go دو زبان را از زاویههای گستردهتری مقایسه میکند.
اشتباههایی که برنامه را قفل میکنند یا از کار میاندازند
- مقدار صفر آمادهی کار نیست. یک
همگام سازی.قفل {}، یکاتمی.صحیح۶۴ {}یا کانالی که هرگز مقداردهی نشده، در نخستین استفاده از کار میافتد. آنها را باهمگام سازی.قفل جدید()،اتمی.صحیح۶۴ جدید(۰)،کانال.جدید(ن)یامقداردهی(ن)بسازید؛ متغیرهای سراسری را در همان خط تعریف یا در ابتدایریشه. - قفل بازورود (reentrant) نیست. قفل کردن دوبارهی آن روی همان نخ، یا صدا زدن
همگام سازی.انجامدرون همانیک بار، برنامه را برای همیشه منتظر نگه میدارد. - هر نخ را دقیقاً یک بار پیوند بزنید و
نابودسازیرا فقط پس از پیوند آخرین استفادهکننده صدا بزنید. - لامبدا متغیرها را بهصورت کپی میگیرد. بدنهای که به
برای موازیداده میشود نمیتواند به یک جمع محلی چیزی اضافه کند؛ در یک خانه، یک متغیر اتمی یا ساختاری قفلدار بنویسید. - پس از پیوندها چاپ کنید. خطهایی که از چند نخ چاپ میشوند به شکلی پیشبینیناپذیر در هم میروند.
- مفسر مرجع نیست. نخها در پشتیبانهای (Backend) کامپایلی بهطور کامل پشتیبانی میشوند؛ کد همزمان را با
salam runیاsalam buildآزمایش کنید و وقتی چیزی درست به نظر نمیرسد، از بررسیگر حافظه کمک بگیرید.
پرسشهای پرتکرار دربارهی همزمانی در زبان سلام
آیا زبان سلام گوروتین دارد؟
سلام کلیدواژهی برو را دارد که یک فراخوانی را روی استخری مشترک و محدود از نخهای سیستمعامل، یکی برای هر هسته، اجرا میکند. اینها نخ سبز نیستند: وظیفهای که منتظر چیزی است نخش را نگه میدارد. برای بیشتر کارهای محاسباتی رفتارش شبیه گوروتین است؛ برای تعداد بسیار زیادی وظیفهی مسدودشونده نه.
آیا زبان برنامهنویسی سلام async/await دارد؟
نه. همزمانی در سلام با نخ، استخر وظیفه، کانال و آینده (استخر.ناهمزمان و استخر.منتظر ماندن) انجام میشود. سرور HTTP سلام روی لینوکس درون خودش از حلقهی رویداد epoll استفاده میکند، پس کد گردانندهها (handler) همگام و ساده باقی میماند.
آیا کد سلام از مسابقهی داده (Data Race) در امان است؟
خودبهخود نه. کامپایلر بررسی نمیکند که نخها داده را چگونه به اشتراک میگذارند، پس محافظت از وضعیت مشترک با قفل، متغیر اتمی یا کانال بر عهدهی خود شماست، درست مثل C، Go و جاوا.
یک برنامهی سلام از چند نخ باید استفاده کند؟
برای کار محاسباتی، تقریباً یکی برای هر هسته: استخر.تعداد پردازنده() تعداد هستهها را میگوید و برو، استخر.برای موازی خودکار و استخر.جدید خودکار خودشان را به همین اندازه تنظیم میکنند. برای ورودی و خروجی مسدودکننده، استخری محدود به اندازهی بار کاری خودتان بسازید و نخ.تعیین سقف را بهعنوان تور ایمنی نگه دارید.