درس ۲۳ از ۴۹ · برنامه‌های بزرگ‌تر

کار همزمان

برنامه‌های سلام می‌توانند چند کار را هم‌زمان روی هسته‌های مختلف پردازنده انجام دهند. این درس یک نخ (رشته‌ی اجرایی) می‌سازد، به آن داده می‌دهد و بعد ابزارهایی را نشان می‌دهد که جلوی تداخل نخ‌ها با هم را می‌گیرند.

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

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

threads.salam
ناپایا پیام := ""

تابع کارگر():
    پیام = "کار در رشته‌ی دیگر انجام شد"
پایان

تابع ریشه:
    ر := نخ‌ساز(کارگر)
    نخ‌پیوند(ر)
    سرچاپ پیام
پایان
خروجیکار در رشته‌ی دیگر انجام شد

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

فرستادن داده به نخ

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

threads-arg.salam
واردسازی حافظه

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

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

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

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

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

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

وقتی دو نخ یک متغیر را تغییر می‌دهند، تغییرهایشان ممکن است با هم برخورد کند. شمار += ۱ در واقع سه گام است (خواندن، افزودن، نوشتن) و اگر دو نخ هم‌زمان همان مقدار قدیمی را بخوانند، یکی از افزایش‌ها گم می‌شود. اگر قفل را از مثال بعدی بردارید، جمع کمتر از ۲۰۰۰۰۰ می‌شود و هر بار هم عدد متفاوتی است.

قفلِ کتابخانه‌ی همگام سازی این مشکل را حل می‌کند: بین همگام سازی.قفل کردن و همگام سازی.باز کردن قفل در هر لحظه فقط یک نخ می‌تواند باشد. این بخش را کوتاه نگه دارید و وقتی برنامه دیگر به قفل نیاز ندارد، آن را با همگام سازی.نابودسازی آزاد کنید.

threads-mutex.salam
واردسازی همگام سازی

ناپایا شمار := ۰
ناپایا قفل := همگام سازی.قفل جدید()

تابع بیفزا():
    تکرار ۱۰۰۰۰۰:
        همگام سازی.قفل کردن(قفل)
        شمار++
        همگام سازی.باز کردن قفل(قفل)
    پایان
پایان

تابع ریشه:
    ن۱ := نخ‌ساز(بیفزا)
    ن۲ := نخ‌ساز(بیفزا)
    نخ‌پیوند(ن۱)
    نخ‌پیوند(ن۲)
    سرچاپ شمار
    همگام سازی.نابودسازی(قفل)
پایان
خروجی200000

شمارنده‌ی اتمی

برای یک عدد که نخ‌های زیادی آن را زیاد می‌کنند، کتابخانه‌ی اتمی سبک‌تر از قفل است. اتمی.افزودن خواندن، افزودن و نوشتن را در یک گام انجام می‌دهد که هیچ چیز نمی‌تواند وسطش بیاید. اتمی.بارگذاری مقدار را می‌خواند و ذخیره، تعویض و مقایسه سپس تعویض بقیه‌ی نیازهای رایج را پوشش می‌دهند.

threads-atomic.salam
واردسازی اتمی

ناپایا بازدید := اتمی.صحیح۶۴ جدید(۰)

تابع سر بزن():
    تکرار ۱۰۰۰۰۰:
        اتمی.افزودن(بازدید، ۱)
    پایان
پایان

تابع ریشه:
    ن۱ := نخ‌ساز(سر بزن)
    ن۲ := نخ‌ساز(سر بزن)
    ن۳ := نخ‌ساز(سر بزن)
    نخ‌پیوند(ن۱)
    نخ‌پیوند(ن۲)
    نخ‌پیوند(ن۳)
    سرچاپ اتمی.بارگذاری(بازدید)
    اتمی.نابودسازی صحیح۶۴(بازدید)
پایان
خروجی300000

منتظر ماندن برای یک گروه

وقتی نخ‌ها زیاد می‌شوند، یک متغیر برای هر شناسه دست‌وپاگیر است. شناسه‌ها را در یک وکتور<صحیح۶۴> بگذارید و در یک حلقه پیوندشان بزنید. گروه انتظار (همگام سازی.گروه انتظار) به پرسشی نزدیک به این پاسخ می‌دهد: «آیا همه سهم خودشان را تمام کرده‌اند؟». همگام سازی.افزودن تعداد را تعیین می‌کند، هر کارگر پس از پایان کارش همگام سازی.اتمام را صدا می‌زند و همگام سازی.انتظار تا صفر شدن شمارش صبر می‌کند.

threads-waitgroup.salam
واردسازی همگام سازی

ناپایا انجام‌شده := ۰
ناپایا قفل := همگام سازی.قفل جدید()
ناپایا گروه := همگام سازی.گروه انتظار جدید()

تابع کار():
    همگام سازی.قفل کردن(قفل)
    انجام‌شده++
    همگام سازی.باز کردن قفل(قفل)
    همگام سازی.اتمام(گروه)
پایان

تابع ریشه:
    ناپایا نخ‌ها := وکتور {} برگردان وکتور<صحیح۶۴>
    دیرکن نخ‌ها.آزادکن()
    همگام سازی.افزودن(گروه، ۵)
    تکرار ۵:
        نخ‌ها.بیفزا(نخ‌ساز(کار))
    پایان
    همگام سازی.انتظار(گروه)
    سرچاپ "تمام شد:"، انجام‌شده
    هر ر در نخ‌ها:
        نخ‌پیوند(ر)
    پایان
    همگام سازی.نابودسازی گروه انتظار(گروه)
    همگام سازی.نابودسازی(قفل)
پایان
خروجیتمام شد: 5

کانال

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

threads-chan.salam
واردسازی کانال

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

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

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

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

حلقه‌ی موازی

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

threads-pool.salam
واردسازی استخر
واردسازی حافظه

ناپایا مربع‌ها := پوچ برگردان صحیح۶۴*

تابع ریشه:
    ن := ۸
    مربع‌ها = حافظه.تخصیص صفرشده(اندازه‌گونه(صحیح۶۴) * (ن برگردان طبیعی۶۴)) برگردان صحیح۶۴*
    استخر.برای موازی(ن، ۴، (ای: صحیح۶۴): مربع‌ها[ای] = ای * ای پایان)
    ناپایا جمع := ۰ برگردان صحیح۶۴
    تکرار ن در ای:
        جمع += مربع‌ها[ای]
    پایان
    سرچاپ "آخرین مربع:"، مربع‌ها[ن - ۱]
    سرچاپ "جمع:"، جمع
    حافظه.آزادسازی(مربع‌ها برگردان تهی*)
پایان
خروجیآخرین مربع: 49 جمع: 140

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

وظیفه‌ها با برو

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

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

تابع ریشه:
    تکرار 100 در i:
        برو کار(i)
    پایان
    استخر.انتظار برو()
پایان

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

ابزارهای دیگر

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

چند قاعده‌ی کلی

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

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