JavaRush /وبلاگ جاوا /Random-FA /مدیریت جریان. کلمه کلیدی فرار و متد yield().

مدیریت جریان. کلمه کلیدی فرار و متد yield().

در گروه منتشر شد
سلام! ما به مطالعه multithreading ادامه می دهیم و امروز با یک کلمه کلیدی جدید - volatile و متد yield() آشنا می شویم. بیا بفهمیم چیه :)

کلیدواژه فرار

هنگام ایجاد برنامه های چند رشته ای، می توانیم با دو مشکل جدی روبرو شویم. در مرحله اول، در حین کار یک برنامه چند رشته ای، رشته های مختلف می توانند مقادیر متغیرها را ذخیره کنند (ما در سخنرانی "استفاده از فرار" بیشتر در مورد این صحبت خواهیم کرد ). ممکن است یک رشته مقدار یک متغیر را تغییر داده باشد، اما دومی این تغییر را مشاهده نکرده است زیرا با کپی حافظه پنهان خود از متغیر کار می‌کند. به طور طبیعی، عواقب آن می تواند جدی باشد. تصور کنید که این فقط نوعی "متغیر" نیست، بلکه، به عنوان مثال، موجودی کارت بانکی شما، که ناگهان شروع به پرش تصادفی به جلو و عقب کرد :) خیلی خوشایند نیست، درست است؟ ثانیاً، در جاوا، عملیات خواندن و نوشتن روی فیلدهای همه نوع به جز longو doubleاتمی هستند. اتمی چیست؟ خوب، به عنوان مثال، اگر مقدار یک متغیر را در یک رشته تغییر دهید int، و در رشته دیگر مقدار این متغیر را بخوانید، یا مقدار قدیمی آن یا یک مقدار جدید را دریافت خواهید کرد - چیزی که پس از تغییر در مشخص شد. موضوع 1. هیچ "گزینه های میانی" در آنجا ظاهر نمی شود شاید. با این حال، این با longو doubleکار نمی کند . چرا؟ چون چند پلتفرمی است. آیا به یاد دارید که چگونه در سطوح اول گفتیم که اصل جاوا "یک بار نوشته می شود، همه جا کار می کند"؟ این کراس پلتفرم است. یعنی یک برنامه جاوا روی پلتفرم های کاملا متفاوت اجرا می شود. به عنوان مثال، در سیستم عامل های ویندوز، نسخه های مختلف لینوکس یا MacOS، و در همه جا این برنامه به طور پایدار کار خواهد کرد. longو double- "سنگین ترین" اولیه در جاوا: وزن آنها 64 بیت است. و برخی از سیستم عامل های 32 بیتی به سادگی اتمی خواندن و نوشتن متغیرهای 64 بیتی را پیاده سازی نمی کنند. چنین متغیرهایی در دو عملیات خوانده و نوشته می شوند. ابتدا 32 بیت اول روی متغیر نوشته می شود، سپس 32 بیت دیگر. بر این اساس، در این موارد ممکن است مشکلی ایجاد شود. یک رشته مقداری 64 بیتی روی یک متغیر می نویسدХ، و او این کار را "در دو مرحله" انجام می دهد. در همان زمان، رشته دوم سعی می کند مقدار این متغیر را بخواند و این کار را درست در وسط انجام می دهد، زمانی که 32 بیت اول قبلا نوشته شده است، اما بیت دوم هنوز نوشته نشده است. در نتیجه یک مقدار متوسط ​​و نادرست را می خواند و خطایی رخ می دهد. به عنوان مثال، اگر در چنین پلتفرمی سعی کنیم یک عدد را روی یک متغیر بنویسیم - 9223372036854775809 - 64 بیت را اشغال می کند. In binary form it will look like this: 100000000000000000000000000000000000000000000000000000000000000001 The first thread will start writing this number to a variable, and will first write the first 32 bits: 1000000000000000000000000000 00000 and then the second 32: 00000000000000000000000000000001 And a second thread can wedge into this gap and مقدار متوسط ​​متغیر را بخوانید - 1000000000000000000000000000000 ، 32 بیت اول که قبلاً نوشته شده است. در سیستم اعشاری این عدد برابر با 2147483648 است یعنی فقط می خواستیم عدد 9223372036854775809 را در یک متغیر بنویسیم اما به دلیل اتمی نبودن این عمل در برخی از پلتفرم ها به عدد "چپ" 2147483648 رسیدیم. ، که ما به هیچ وجه به آن نیاز نداریم و مشخص نیست که چگونه بر عملکرد برنامه تأثیر می گذارد. رشته دوم به سادگی مقدار متغیر را قبل از اینکه در نهایت نوشته شود خواند، یعنی 32 بیت اول را دید، اما 32 بیت دوم را نه. البته این مشکلات دیروز به وجود نیامدند و در جاوا فقط با استفاده از یک کلمه کلیدی حل می شوند - volatile . اگر در برنامه خود متغیری را با کلمه volatile اعلام کنیم...
public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…این به آن معنا است:
  1. همیشه به صورت اتمی خوانده و نوشته خواهد شد. حتی اگر 64 بیتی doubleیا long.
  2. ماشین جاوا آن را کش نمی کند. بنابراین وضعیتی که 10 رشته با نسخه های محلی خود کار می کنند، حذف می شود.
اینجوری دو مشکل خیلی جدی در یک کلمه حل میشه :)

متد yield().

ما قبلاً به بسیاری از متدهای کلاس نگاه کرده ایم Thread، اما یک روش مهم وجود دارد که برای شما جدید خواهد بود. این روش yield() است . از انگلیسی به عنوان "تسلیم شدن" ترجمه شده است. و این دقیقاً همان کاری است که روش انجام می دهد! مدیریت جریان.  کلمه کلیدی فرار و متد yield() - 2وقتی روش بازدهی را روی یک نخ صدا می‌زنیم، در واقع به رشته‌های دیگر می‌گوید: «بسیار خوب، بچه‌ها، من عجله خاصی ندارم، بنابراین اگر برای هر یک از شما مهم است که زمان CPU را دریافت کنید، آن را بگیرید، من هستم. فوری نیست.» در اینجا یک مثال ساده از نحوه عملکرد آن آورده شده است:
public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + "give way to others");
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
ما به طور متوالی سه موضوع را ایجاد و راه اندازی می کنیم Thread-0- Thread-1و Thread-2. Thread-0ابتدا شروع می شود و بلافاصله جای خود را به دیگران می دهد. پس از آن شروع می شود Thread-1، و همچنین راه می دهد. پس از آن، شروع می شود Thread-2، که آن نیز پایین تر است. ما هیچ رشته‌ای دیگر نداریم، و پس از اینکه Thread-2آخرین موضوع جای خود را رها کرد، زمان‌بندی رشته به نظر می‌رسد: «پس، رشته‌های جدیدی دیگر وجود ندارد، چه کسی را در صف داریم؟ آخرین کسی که قبلاً جای خود را رها کرد چه کسی بود Thread-2؟ فکر کنم بود Thread-1؟ خوب، پس بگذار این کار انجام شود.» Thread-1کار خود را تا انتها انجام می دهد، پس از آن زمانبندی رشته به هماهنگی ادامه می دهد: "خوب، Thread-1 تکمیل شد. آیا کس دیگری در صف داریم؟" Thread-0 در صف وجود دارد: بلافاصله قبل از Thread-1 جای خود را رها کرد. حالا موضوع به او رسیده است و او را تا آخر پیش می برند. پس از آن زمان‌بندی‌کننده هماهنگی رشته‌ها را به پایان می‌رساند: «خوب، Thread-2، شما جای خود را به رشته‌های دیگر دادید، همه آنها قبلاً کار کرده‌اند. تو آخرین کسی بودی که راه را واگذار کردی، پس حالا نوبت توست.» پس از این، Thread-2 تا کامل شدن اجرا می شود. خروجی کنسول به این صورت خواهد بود: Thread-0 جای خود را به دیگران می دهد. Thread-1 جای خود را به دیگران می دهد Thread-2 جای خود را به دیگران می دهد. Thread-1 به پایان رسیده است. اجرای Thread-0 به پایان رسیده است. اجرای Thread-2 به پایان رسیده است. البته زمان‌بندی رشته می‌تواند رشته‌ها را به ترتیب متفاوتی اجرا کند (مثلاً 2-1-0 به جای 0-1-2)، اما اصل یکسان است.

اتفاق می افتد-قبل از قوانین

آخرین چیزی که امروز به آن خواهیم پرداخت، اصول " پیش از وقوع " است. همانطور که می دانید در جاوا، بیشتر کار تخصیص زمان و منابع به Thread ها برای تکمیل وظایف آنها توسط Thread Scheduler انجام می شود. همچنین، شما بیش از یک بار مشاهده کرده‌اید که چگونه نخ‌ها به ترتیب دلخواه اجرا می‌شوند و اغلب پیش‌بینی آن غیرممکن است. و به طور کلی، پس از برنامه نویسی "متوالی" که قبلا انجام دادیم، چند رشته ای به نظر یک چیز تصادفی است. همانطور که قبلاً دیدید، پیشرفت یک برنامه چند رشته ای را می توان با استفاده از مجموعه ای کامل از روش ها کنترل کرد. اما علاوه بر این، در چند رشته ای جاوا "جزیره ثبات" دیگری وجود دارد - 4 قانون به نام " پیش از وقوع ". به معنای واقعی کلمه از انگلیسی این به عنوان "پیش از این اتفاق می افتد" یا "پیش از این اتفاق می افتد" ترجمه شده است. درک معنای این قوانین بسیار ساده است. تصور کنید که ما دو رشته داریم - Aو B. هر یک از این رشته ها می توانند عملیات 1و 2. و هنگامی که در هر یک از قوانین می گوییم " A اتفاق می افتد قبل از B "، به این معنی است که تمام تغییرات ایجاد شده توسط thread Aقبل از عملیات 1و تغییراتی که این عملیات به همراه دارد Bدر زمان انجام عملیات 2برای نخ قابل مشاهده است. پس از انجام عمل هر یک از این قوانین تضمین می کند که هنگام نوشتن یک برنامه چند رشته ای، برخی از رویدادها قبل از دیگران در 100٪ مواقع اتفاق می افتد و نخ Bدر زمان عملیات 2همیشه از تغییراتی که thread Аدر طول عملیات ایجاد کرده است آگاه باشد. 1. بیایید به آنها نگاه کنیم.

قانون 1.

انتشار یک mutex قبل از اینکه thread دیگری همان مانیتور را بدست آورد، اتفاق می افتد. خب، اینجا همه چیز واضح به نظر می رسد. اگر mutex یک شی یا کلاس توسط یک thread، به عنوان مثال، یک thread به دست آید А، یک رشته دیگر (thread B) نمی تواند آن را در همان زمان بدست آورد. باید منتظر بمانید تا mutex آزاد شود.

قانون 2.

روش Thread.start() قبل اتفاق می افتد Thread.run() . هیچ چیز پیچیده ای هم نیست. قبلاً می دانید: برای اینکه کد داخل متد شروع به اجرا کند run()، باید متد موجود در thread را فراخوانی کنید start(). این مال اوست و نه خود روش run()! این قانون تضمین می کند که Thread.start()مقادیر تمام متغیرهای تنظیم شده قبل از اجرا در متدی که اجرا را شروع کرده است قابل مشاهده خواهد بود run().

قانون 3.

تکمیل روش run() قبل از خروج متد اتفاق می افتد join(). بیایید به دو جریان خود بازگردیم - Аو B. ما متد را join()به گونه ای فراخوانی می کنیم که thread قبل از انجام کار Bباید تا اتمام صبر کند . Aاین بدان معنی است که روش run()شیء A قطعاً تا انتها اجرا خواهد شد. و تمام تغییرات داده هایی که در روش run()thread رخ می دهد ، زمانی که Thread منتظر تکمیل شدن و شروع به کار خود باشد، Aکاملاً در Thread قابل مشاهده خواهد بود .BA

قانون 4.

نوشتن روی یک متغیر فرار قبل از خواندن از همان متغیر اتفاق می افتد. با استفاده از کلمه کلیدی فرار، در واقع همیشه مقدار فعلی را دریافت می کنیم. حتی در مورد longو double، مشکلاتی که قبلاً در مورد آنها صحبت شد. همانطور که قبلاً متوجه شدید، تغییرات ایجاد شده در برخی از رشته ها همیشه برای رشته های دیگر قابل مشاهده نیست. اما، البته، اغلب اوقات شرایطی وجود دارد که چنین رفتار برنامه ای مناسب ما نیست. فرض کنید به یک متغیر در یک رشته یک مقدار اختصاص داده ایم A:
int z;.

z= 555;
اگر Bقرار بود thread ما مقدار یک متغیر را zدر کنسول چاپ کند، به راحتی می تواند 0 را چاپ کند زیرا از مقدار اختصاص داده شده به آن اطلاعی ندارد. بنابراین، قانون 4 به ما تضمین می کند: اگر یک متغیر را فرار اعلام کنید z، تغییرات مقادیر آن در یک رشته همیشه در رشته دیگر قابل مشاهده است. اگر کلمه volatile را به کد قبلی اضافه کنیم ...
volatile int z;.

z= 555;
... وضعیتی که جریان Bخروجی 0 را به کنسول می دهد مستثنی است. نوشتن بر روی متغیرهای فرار قبل از خواندن از آنها اتفاق می افتد.
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION