برنامه‌نویسی هم‌زمان در زبان سلام: نخ‌ها، استخر وظیفه، کانال‌ها و همگام‌سازی

هم‌زمانی در سلام روی نخ‌های سیستم‌عامل و یک استخر وظیفه‌ی محدود ساخته شده است. این راهنما همه‌ی ابزارها را، از نخ‌ساز و نخ‌پیوند تا کانال، گزینش، گروه و آینده، با برنامه‌های اجراشدنی نشان می‌دهد و این مدل را با Go، Rust و جاوا مقایسه می‌کند.

در این مقاله
  1. مدل هم‌زمانی زبان سلام: نخ سیستم‌عامل، استخر و بدون async/await
  2. نخ‌ها با نخ‌ساز و نخ‌پیوند
  3. کلیدواژه‌ی برو و داده‌ی مشترک با قفل
  4. حلقه‌ی موازی در سلام: برای موازی و نگاشت موازی
  5. گروه خطا و آینده (Future)
  6. کانال و گزینش در زبان سلام
  7. لغو، مهلت و دیگر ابزارهای همگام‌سازی
  8. مقایسه‌ی هم‌زمانی در سلام با Go، Rust و جاوا
  9. اشتباه‌هایی که برنامه را قفل می‌کنند یا از کار می‌اندازند
  10. پرسش‌های پرتکرار درباره‌ی هم‌زمانی در زبان سلام

هم‌زمانی (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 استفاده می‌کند.

نخ‌ها با نخ‌ساز و نخ‌پیوند

نخ‌ساز(تابع) تابع را روی یک نخ تازه شروع می‌کند و یک شناسه برمی‌گرداند؛ نخ‌پیوند(شناسه) منتظر می‌ماند تا آن نخ تمام شود. تابع نخ یا ورودی ندارد یا دقیقاً یک اشاره‌گر می‌گیرد: نخ‌ساز(تابع، داده). همین اشاره‌گر راهی است که هر نخ سهم خودش از کار را بگیرد و نتیجه‌اش را همان‌جا بگذارد. داده باید تا پایان کار نخ زنده بماند، پس در هیپ ساخته می‌شود.

سلام
واردسازی حافظه

ساختار بازه:
    همگانی آغاز: صحیح۶۴ = ۰
    همگانی انتها: صحیح۶۴ = ۰
    همگانی مجموع: صحیح۶۴ = ۰
پایان

تابع جمع مربع‌ها(ب: بازه*):
    ناپایا جمع := ۰ برگردان صحیح۶۴
    تکرار ب[۰].آغاز تا ب[۰].انتها در ای:
        جمع += ای * ای
    پایان
    ب[۰].مجموع = جمع
پایان

تابع بازه‌ی تازه(اول: صحیح۶۴، آخر: صحیح۶۴): بازه*:
    ب := حافظه.تخصیص(اندازه‌گونه(بازه)) برگردان بازه*
    ب[۰] = بازه { آغاز = اول، انتها = آخر }
    برگشت ب
پایان

تابع ریشه:
    پایین := بازه‌ی تازه(۱، ۵۰۰۰۰۰)
    بالا := بازه‌ی تازه(۵۰۰۰۰۱، ۱۰۰۰۰۰۰)
    ن۱ := نخ‌ساز(جمع مربع‌ها، پایین)
    ن۲ := نخ‌ساز(جمع مربع‌ها، بالا)
    نخ‌پیوند(ن۱)
    نخ‌پیوند(ن۲)
    سرچاپ "نیمه‌ی اول:"، پایین[۰].مجموع
    سرچاپ "نیمه‌ی دوم:"، بالا[۰].مجموع
    سرچاپ "جمع کل:"، پایین[۰].مجموع + بالا[۰].مجموع
    حافظه.آزادسازی(پایین برگردان تهی*)
    حافظه.آزادسازی(بالا برگردان تهی*)
پایان
خروجینیمه‌ی اول: 41666791666750000 نیمه‌ی دوم: 291667041666750000 جمع کل: 333333833333500000

این دو نخ هرگز به حافظه‌ی مشترکی دست نمی‌زنند، پس به هیچ هماهنگی‌ای نیاز ندارند؛ این ساده‌ترین نوع برنامه‌ی هم‌زمان برای درست نوشتن است. هر نخ را دقیقاً یک بار پیوند بزنید و نتیجه‌اش را فقط پس از پیوند بخوانید؛ نخ‌پیوند دوم روی همان شناسه از حافظه‌ی آزادشده استفاده می‌کند. نخ.تعیین سقف(ن) تعداد نخ‌هایی را که هم‌زمان می‌توانند زنده باشند محدود می‌کند و پس از رسیدن به سقف، نخ‌ساز به جای تمام کردن منابع سیستم، صفر برمی‌گرداند. نخ.اجرا با پشته هم نخی با پشته‌ی بزرگ‌تر برای بازگشت‌های عمیق می‌سازد.

کلیدواژه‌ی برو و داده‌ی مشترک با قفل

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

وقتی وظیفه‌ها یک داده را تغییر می‌دهند، به قفل نیاز دارند. در برنامه‌ی زیر ۳٬۰۰۰ وظیفه کدهای وضعیت HTTP را در یک نگاشت مشترک می‌شمارند و یک قفل (Mutex) از آن محافظت می‌کند:

سلام
واردسازی استخر
واردسازی همگام سازی

ناپایا قفل := همگام سازی.قفل جدید()
ناپایا شمار وضعیت := نگاشت {} برگردان نگاشت<صحیح، صحیح>

تابع ثبت(کد: صحیح):
    همگام سازی.قفل کردن(قفل)
    شمار وضعیت.درج(کد، شمار وضعیت.دریافت با پیش فرض(کد، ۰) + ۱)
    همگام سازی.باز کردن قفل(قفل)
پایان

تابع ریشه:
    تکرار ۳۰۰۰ در ای:
        ناپایا کد := ۲۰۰
        اگر ای % ۱۰ == ۰:
            کد = ۴۰۴
        پایان
        اگر ای % ۱۰۰ == ۰:
            کد = ۵۰۰
        پایان
        برو ثبت(کد)
    پایان
    استخر.انتظار برو()
    سرچاپ "۲۰۰:"، شمار وضعیت.دریافت با پیش فرض(۲۰۰، ۰)
    سرچاپ "۴۰۴:"، شمار وضعیت.دریافت با پیش فرض(۴۰۴، ۰)
    سرچاپ "۵۰۰:"، شمار وضعیت.دریافت با پیش فرض(۵۰۰، ۰)
    همگام سازی.نابودسازی(قفل)
    شمار وضعیت.آزادکن()
پایان
خروجی۲۰۰: 2700 ۴۰۴: 270 ۵۰۰: 30

بدون قفل کردن و باز کردن قفل، دو وظیفه ممکن است هر دو همان شمارش قدیمی را بخوانند و هر دو یکی به آن اضافه کنند، و دو درج هم‌زمان حتی می‌تواند خود نگاشت را خراب کند. برای یک شمارنده‌ی تنها، اتمی.افزودن روی یک اتمی.صحیح۶۴ خواندن، افزودن و نوشتن را در یک گام تجزیه‌ناپذیر انجام می‌دهد و از قفل سبک‌تر است. کتابخانه‌ی اتمی همه‌جا بدون قفل کار می‌کند، جز وقتی برنامه با tcc ساخته شود؛ آن‌جا برای هر خانه یک قفل با همان معنا به کار می‌رود. برو کلیدواژه‌ای وابسته به جایگاه است، پس متغیری به نام برو همچنان کار می‌کند؛ در برنامه‌های انگلیسی همین کلیدواژه go نوشته می‌شود.

حلقه‌ی موازی در سلام: برای موازی و نگاشت موازی

خیلی وقت‌ها تنها چیزی که می‌خواهید این است که بدنه‌ی یک حلقه را روی اندیس‌های زیاد و میان همه‌ی هسته‌ها پخش کنید. استخر.برای موازی(ن، کارگرها، بدنه) بدنه را برای هر ای از ۰ تا ن - ۱ صدا می‌زند و وقتی همه تمام شدند برمی‌گردد؛ استخر.نگاشت موازی همین کار را می‌کند و نتیجه‌ی هر فراخوانی را در یک وکتور<صحیح۶۴> جمع می‌کند. برنامه‌ی زیر عددهای اول کمتر از یک میلیون را در ده بلوک می‌شمارد:

سلام
واردسازی استخر

تابع اول است(ن: صحیح۶۴): منطقی:
    اگر ن < ۲: برگشت نادرست پایان
    ناپایا مق := ۲ برگردان صحیح۶۴
    تا مق * مق <= ن:
        اگر ن % مق == ۰: برگشت نادرست پایان
        مق++
    پایان
    برگشت درست
پایان

تابع شمار اول‌ها(بلوک: صحیح۶۴): صحیح۶۴:
    ناپایا یافته := ۰ برگردان صحیح۶۴
    آغاز := بلوک * ۱۰۰۰۰۰
    تکرار آغاز تا آغاز + ۹۹۹۹۹ در ن:
        اگر اول است(ن): یافته++ پایان
    پایان
    برگشت یافته
پایان

تابع ریشه:
    شمارها := استخر.نگاشت موازی(۱۰، استخر.تعداد پردازنده()، (ب: صحیح۶۴): برگشت شمار اول‌ها(ب) پایان)
    ناپایا جمع := ۰ برگردان صحیح۶۴
    تکرار شمارها.طول() در ای:
        جمع += شمارها.بگیر(ای)
    پایان
    سرچاپ "عددهای اول زیر یک میلیون:"، جمع
    سرچاپ "در بلوک اول:"، شمارها.بگیر(۰)، "در بلوک آخر:"، شمارها.بگیر(۹)
    شمارها.آزادکن()
پایان
خروجیعددهای اول زیر یک میلیون: 78498 در بلوک اول: 9592 در بلوک آخر: 7224

هر نتیجه در خانه‌ی خودش می‌نشیند، پس خروجی به ترتیب تمام شدن بلوک‌ها بستگی ندارد. صفحه‌ی مرجع استخر قاعده‌ها را می‌گوید: هرگز به ترتیب نوبت‌ها تکیه نکنید، هر نوبت را آن‌قدر بزرگ بگیرید که ارزش یک نخ را داشته باشد (تکه‌های بزرگ، نه تک‌تک عضوها)، و از درون بدنه‌ی حلقه چاپ نکنید. به تا مق * مق <= ن: در تابع اول است دقت کنید: تا در سلام حلقه را تا وقتی شرط درست است ادامه می‌دهد. در فارسی این معنا طبیعی است، اما معادل انگلیسی آن، until، برنامه‌نویسان Ruby را غافلگیر می‌کند، چون در Ruby برعکس رفتار می‌کند.

گروه خطا و آینده (Future)

دو الگوی آشنا، errgroup در Go و Future در جاوا، در خود کتابخانه‌ی استخر وجود دارند. یک گروه وظیفه‌ها را با سقفی برای تعداد اجرای هم‌زمان اجرا می‌کند و نخستین خطا را، به شکل یک رشته‌ی ناتهی، نگه می‌دارد. ناهمزمان یک وظیفه را روی استخر اجرا می‌کند و یک آینده (Future) برمی‌گرداند که با منتظر ماندن نتیجه‌اش را می‌گیرید:

سلام
واردسازی استخر

تابع بررسی(ردیف: صحیح): رشته:
    اگر ردیف == ۱۳:
        برگشت "ردیف ۱۳: قیمت منفی است"
    پایان
    برگشت ""
پایان

تابع جمع کند(ن: صحیح۶۴): صحیح۶۴:
    ناپایا ج := ۰ برگردان صحیح۶۴
    تکرار ن در ای:
        ج += ای
    پایان
    برگشت ج
پایان

تابع ریشه:
    گروه := استخر.گروه کار جدید(۴)
    تکرار ۵۰ در ردیف:
        استخر.گروه برو(گروه، (): برگشت بررسی(ردیف) پایان)
    پایان
    سرچاپ "نخستین خطا:"، استخر.گروه انتظار(گروه)
    استخر.گروه توقف(گروه)

    است := استخر.جدید(۲، ۸)
    الف := استخر.ناهمزمان(است، (): برگشت جمع کند(۱۰۰۰۰۰۰) پایان)
    ب := استخر.ناهمزمان(است، (): برگشت جمع کند(۲۰۰۰) پایان)
    سرچاپ "الف ="، استخر.منتظر ماندن(الف)، "ب ="، استخر.منتظر ماندن(ب)
    استخر.رها کردن(الف)
    استخر.رها کردن(ب)
    استخر.توقف(است)
پایان
خروجینخستین خطا: ردیف ۱۳: قیمت منفی است الف = 499999500000 ب = 1999000

استخر.جدید(۲، ۸) استخری با دو کارگر و جا برای هشت وظیفه‌ی منتظر می‌سازد و تعداد نخ‌هایش هرگز بیشتر نمی‌شود. استخر.جدید کشسان(کمینه، بیشینه، صف) وقتی همه‌ی کارگرها مشغول‌اند تا بیشینه کارگر اضافه می‌کند و برای وظیفه‌هایی که ممکن است منتظر یکدیگر بمانند انتخاب درستی است. چون وظیفه‌ها نخ‌اند، یک وظیفه روی استخر ثابتِ پر نباید هرگز منتظر وظیفه‌ای بماند که پشت سرش در صف است؛ استخر.ارسال با اجرای مستقیم وقتی صف پر است وظیفه را روی همان نخ فراخواننده اجرا می‌کند و برای وظیفه‌هایی که خودشان وظیفه می‌فرستند امن است.

کانال و گزینش در زبان سلام

کانالِ کتابخانه‌ی کانال مقدارها را از یک نخ به نخ دیگر می‌برد، و نخ‌ها جز خود کانال هیچ چیز مشترکی ندارند. مقداردهی(ن) به کانال جای ن مقدار می‌دهد؛ مقداردهی(۰) کانالی ملاقاتی (rendezvous) می‌سازد که در آن فرستادن صبر می‌کند تا گیرنده‌ای مقدار را بردارد. پس از بستن() دیگر چیزی فرستاده نمی‌شود، و وقتی مقدارهای باقی‌مانده برداشته شدند، دریافت ورودی باز را نادرست می‌کند؛ این برای کارگر نشانه‌ی توقف است.

سلام
واردسازی کانال

ناپایا کارها := کانال.کانال {} برگردان کانال.کانال<صحیح۶۴>
ناپایا نتیجه‌ها := کانال.کانال {} برگردان کانال.کانال<صحیح۶۴>

تابع کارگر():
    ناپایا باز := درست
    تا درست:
        ن := کارها.دریافت(باز)
        اگر وارونه باز:
            بشکن
        پایان
        نتیجه‌ها.فرستادن(ن * ن)
    پایان
پایان

تابع ریشه:
    کارها.مقداردهی(۸)
    نتیجه‌ها.مقداردهی(۱۰۰)
    ناپایا کارگرها := وکتور {} برگردان وکتور<صحیح۶۴>
    دیرکن کارگرها.آزادکن()
    تکرار ۳:
        کارگرها.بیفزا(نخ‌ساز(کارگر))
    پایان
    تکرار ۱ تا ۱۰۰ در ای:
        کارها.فرستادن(ای برگردان صحیح۶۴)
    پایان
    کارها.بستن()
    هر ک در کارگرها:
        نخ‌پیوند(ک)
    پایان
    نتیجه‌ها.بستن()
    همه := کانال.تخلیه(نتیجه‌ها)
    ناپایا جمع := ۰ برگردان صحیح۶۴
    هر م در همه:
        جمع += م
    پایان
    سرچاپ "تعداد نتیجه‌ها:"، همه.طول()، "جمع:"، جمع
    همه.آزادسازی()
    کارها.آزادسازی()
    نتیجه‌ها.آزادسازی()
پایان
خروجیتعداد نتیجه‌ها: 100 جمع: 338350

کانال.گزینش هم‌زمان منتظر چند کانال می‌ماند و شماره‌ی کانالی را برمی‌گرداند که مقدار داده است؛ اگر مهلت تمام شود -1 و اگر همه‌ی کانال‌ها بسته باشند -2 برمی‌گرداند. همراه با کانال.پس از و کانال.ضربان که پس از چند میلی‌ثانیه یا در فاصله‌های منظم مقدار تحویل می‌دهند، الگوهای مهلت و ضربان قلب (heartbeat) را که برنامه‌نویسان Go با select می‌نویسند پوشش می‌دهد:

سلام
واردسازی کانال
واردسازی استخر

تابع ریشه:
    ناپایا تند := کانال.جدید(۱) برگردان کانال.کانال<صحیح۶۴>
    ناپایا کند := کانال.جدید(۱) برگردان کانال.کانال<صحیح۶۴>
    ناپایا دوکانال := وکتور {} برگردان وکتور<کانال.کانال<صحیح۶۴>>
    دیرکن دوکانال.آزادکن()
    دوکانال.بیفزا(تند)
    دوکانال.بیفزا(کند)

    است := استخر.جدید(۲، ۴)
    استخر.ارسال(است، (): تند.فرستادن(۱ برگردان صحیح۶۴) پایان)

    ناپایا م := ۰ برگردان صحیح۶۴
    کدام := کانال.گزینش(دوکانال، م، ۱۰۰۰)
    سرچاپ "کانال"، کدام، "پاسخ داد با مقدار"، م

    کدام۲ := کانال.گزینش(دوکانال، م، ۵۰)
    سرچاپ "گزینش دوم:"، کدام۲

    تند.بستن()
    کند.بستن()
    سرچاپ "پس از بستن:"، کانال.گزینش(دوکانال، م، ۵۰)
    استخر.توقف(است)
    تند.آزادسازی()
    کند.آزادسازی()
پایان
خروجیکانال 0 پاسخ داد با مقدار 1 گزینش دوم: -1 پس از بستن: -2

برخلاف select در Go، این یک تابع کتابخانه‌ای روی کانال‌هایی با یک گونه‌ی عضو است، نه دستوری روی هر عملیات دلخواه کانال. گزینش فرستادن، کوشش برای گزینش و کوشش برای گزینش فرستادن حالت‌های فرستادن و غیرمسدودکننده را پوشش می‌دهند. کانال یک وکتور یا اشاره‌گر را به‌صورت دستگیره کپی می‌کند، پس هر دو نخ به همان چیزی دسترسی دارند که دستگیره به آن اشاره می‌کند.

لغو، مهلت و دیگر ابزارهای همگام‌سازی

بقیه‌ی جعبه‌ابزار از طراحی‌های آشنا پیروی می‌کند:

  • زمینه با با لغو، با مهلت و با موعد درختی از زمینه‌ها می‌سازد. لغو یک گره همه‌ی گره‌های زیرش را تمام می‌کند و موعدِ فرزند فقط می‌تواند موعدی را که به ارث برده کوتاه‌تر کند. کارگرها زمینه.تمام شده را بررسی می‌کنند یا در زمینه.انتظار می‌مانند.
  • همگام سازی.قفل خواندن نوشتن برای خواننده‌های زیاد و نویسنده‌ای گاه‌به‌گاه (با اولویت نویسنده)، همگام سازی.متغیر شرط برای خوابیدن تا برقرار شدن یک شرط، همگام سازی.نشانگر (Semaphore) برای راه دادن حداکثر N نخ به یک بخش، و همگام سازی.یک بار برای آماده‌سازی‌ای که باید دقیقاً یک بار اجرا شود.
  • اتمی.پرچم با آزمون سپس تنظیم برای نشانه‌ی توقفی یک‌باره که دقیقاً یک فراخواننده برنده‌اش می‌شود.
  • رویدادها برای یک پخش‌کننده‌ی رویداد درون‌فرایندی با نخ کارگر اختیاری، و کرون برای کارهای زمان‌بندی‌شده که هر کدام روی نخ خودش اجرا می‌شود.

ابزارهای همگام سازی به جای سرکشی پیاپی (polling) روی یک متغیر شرط منتظر می‌مانند، پس نخی که منتظر است هیچ زمان پردازنده‌ای مصرف نمی‌کند.

مقایسه‌ی هم‌زمانی در سلام با Go، Rust و جاوا

سلامGoRustجاوا
واحد هم‌زمانینخ سیستم‌عامل، استخرهای محدودگوروتین روی یک زمان‌بندنخ سیستم‌عامل، یا وظیفه‌ی 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 و جاوا.

یک برنامه‌ی سلام از چند نخ باید استفاده کند؟

برای کار محاسباتی، تقریباً یکی برای هر هسته: استخر.تعداد پردازنده() تعداد هسته‌ها را می‌گوید و برو، استخر.برای موازی خودکار و استخر.جدید خودکار خودشان را به همین اندازه تنظیم می‌کنند. برای ورودی و خروجی مسدودکننده، استخری محدود به اندازه‌ی بار کاری خودتان بسازید و نخ.تعیین سقف را به‌عنوان تور ایمنی نگه دارید.

ساختن با سلام

ساخت برنامه‌ی دسکتاپ و رابط گرافیکی با زبان سلام: از برنامه‌های WebView تا رابط بومی که خود سلام می‌کشد

سلام سه راه برای آوردن یک پنجره روی صفحه دارد: رابط وب در نمایشگر وب سیستم‌عامل، رابط بومی‌ای که سلام از زبان صفحه می‌کشد، و raylib برای بازی. این‌جا می‌بینید هر کدام چه می‌کند، چطور بسته‌بندی می‌شود و کجا متوقف می‌شود.

خواندن مقاله
ساختن با سلام

از ابزار خط فرمان تا هسته‌ی سیستم‌عامل: برنامه‌نویسی خودایستا و Bare Metal در زبان سلام

همان کامپایلری که سرورهای وب سلام را می‌سازد، می‌تواند برنامه‌ای بسازد که کل ماشین در اختیار خودش است: بدون libc، بدون فراخوان سیستمی، با یک محیط اجرای کوچک و کرنلی که در QEMU بوت می‌شود. این مقاله سازوکارش را و آنچه هنوز باید خودتان بنویسید توضیح می‌دهد.

خواندن مقاله
ساختن با سلام

محاسبات علمی و یادگیری ماشین با زبان سلام: تانسور، ماتریس، مشتق‌گیری خودکار و شبکه‌ی عصبی

تانسور، مشتق‌گیری خودکار معکوس، لایه‌های شبکه‌ی عصبی، بهینه‌سازها، مدل‌هایی به سبک scikit-learn و خواننده‌های ONNX و GGUF: پشته‌ی یادگیری ماشین سلام در کتابخانه‌ی استاندارد است و به کد بومی کامپایل می‌شود. گشتی همراه با برنامه‌های اجراشدنی و فهرستی روشن از کارهایی که انجام نمی‌دهد.

خواندن مقاله