مقایسه CyclicBarrier و CountDownLatch در جاوا — به زبان ساده
در این مقاله به مقایسه CyclicBarrier و CountDownLatch در جاوا میپردازیم و تلاش میکنیم تا شباهتها و تفاوتهای آنها را دریابیم.
CyclicBarrier و CountDownLatch چه هستند؟
زمانی که موضوع «همزمانی» (concurrency) را در جاوا بررسی میکنیم، شاید درک مفهوم کاری که هر یک از گزینههای CyclicBarrier و CountDownLatch انجام میدهند دشوار باشد. پیش از هر نکتهای باید اشاره کنیم که CyclicBarrier و CountDownLatch برای مدیریت اپلیکیشنهای «چندنخی» (multi-thread) استفاده میشوند. هر دوی آنها به منظور بیان شیوه انتظار یک نخ یا گروهی از نخها ارائه شدهاند.
CountDownLatch
با استفاده از CountDownLatch در جاوا یک نخ تا زمانی که شماره نخهای دیگرِ در حال اجرا به صفر نرسیده باشد، صبر میکند. آن را میتوانیم به صورت یک ظرف غذا در رستوران تصور کنیم که در حال آمادهسازی است. مهم نیست که هر آشپز چه تعداد از آیتمهای این ظرف غذا را آماده میکند، در هر صورت پیشخدمت باید صبر کند تا همه آیتمها به ظرف اضافه شوند. اگر یک ظرف غذا دارای n آیتم باشد، هر آشپز پس از آماده کردن یک آیتم، تعداد آیتمها را یک واحد کاهش میدهد تا این که به صفر برسد.
CyclicBarrier
CyclicBarrier یک سازه با قابلیت استفاده مجدد است که در آن یک گروه از نخها همراه با هم منتظر میمانند تا همه نخها برسند. در این زمان یک مانع شکسته میشود و میتوان عملی را انجام داد. CyclicBarrier را میتوان مانند یک گروه از دوستان تصور کرد. هر بار که آنها تصمیم میگیرند تا در یک رستوران غذا بخورند، در یک نقطه جمع میشوند و منتظر هم میمانند. آنها تنها زمانی به رستوران میروند که همه اعضای تیم در آن نقطه جمع شده باشند.
تفاوت وظایف و نخها
در این بخش به مقایسه تفصیلی دو کلاس Tasks و Threads میپردازیم. همچنان که در بخش قبل اشاره کردیم، CyclicBarrier به چند نخ اجازه میدهد که منتظر همدیگر بمانند، در حالی که CountDownLatch به یک یا چند نخ امکان میدهد که منتظر تکمیل شدن یک وظیفه بمانند.
به طور خلاصه CyclicBarrier شماره نخها را نگهداری میکند در حالی که CyclicBarrier شماره وظیفهها را نگه میدارد. در کد زیر یک CountDownLatch را با شماره دو تعریف کردهایم. سپس ()countDown را دو بار از یک نخ منفرد فرا میخوانیم.
1CountDownLatch countDownLatch = new CountDownLatch(2);
2Thread t = new Thread(() -> {
3 countDownLatch.countDown();
4 countDownLatch.countDown();
5});
6t.start();
7countDownLatch.await();
8
9assertEquals(0, countDownLatch.getCount());
زمانی که latch به صفر برسد، فراخوانی به await بازگشت مییابد. توجه کنید که در این حالت میتوانستیم در همان نخ، دو بار شماره را کاهش دهیم، اما CyclicBarrier از این نظر کمی متفاوت است. همانند مثال قبل یک CyclicBarrier را با شماره دو ایجاد میکنیم و ()await را روی آن فرا میخوانیم. این بار این کار را از همان نخ انجام میدهیم.
1CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
2Thread t = new Thread(() -> {
3 try {
4 cyclicBarrier.await();
5 cyclicBarrier.await();
6 } catch (InterruptedException | BrokenBarrierException e) {
7 // error handling
8 }
9});
10t.start();
11
12assertEquals(1, cyclicBarrier.getNumberWaiting());
13assertFalse(cyclicBarrier.isBroken());
نخستین تفاوت این است که نخهایی که منتظر هستند خودشان مانع محسوب میشوند. تفاوت دوم که مهمتر نیز هست، در این است که ()await دوم بیاستفاده است. یک نخ منفرد نمیتواند روی یک مانع دو بار شمارش معکوس کند.
در واقع از آنجا که t باید منتظر نخ دیگری بماند تا ()await را فراخوانی کد و شماره را به دو برساند، فراخانی دوم t به ()await در عمل اجرا نمیشود تا این که مانع شکسته شود.
در تست ما مانع شکسته نشد، زیرا ما تنها یک نخ منتظر داشتیم و دو نخ که برای عبور از مانع لازم بود وجود نداشتند. این موضوع از طریق متد ()cyclicBarrier.isBroken که مقدار false بازگشت میدهد نیز هویدا است.
قابلیت استفاده مجدد
دومین تفاوت آشکار بین این دو کلاس، در قابلیت استفاده مجدد نهفته است. زمانی که در CyclicBarrier از یک مانع عبور میکنیم، شماره به مقدار اولیه ریست میشود. اما CountDownLatch متفاوت است، زیرا شماره هرگز ریست نمیشود. در کد زیر یک CountDownLatch با شماره 7 تعریف کردهایم و آن را با 20 فراخوانی مختلف میشماریم.
1CountDownLatch countDownLatch = new CountDownLatch(7);
2ExecutorService es = Executors.newFixedThreadPool(20);
3for (int i = 0; i < 20; i++) {
4 es.execute(() -> {
5 long prevValue = countDownLatch.getCount();
6 countDownLatch.countDown();
7 if (countDownLatch.getCount() != prevValue) {
8 outputScraper.add("Count Updated");
9 }
10 });
11}
12es.shutdown();
13
14assertTrue(outputScraper.size() <= 7);
به این ترتیب مشاهده میکنیم که علیرغم وجود 20 نخ مختلف که ()countDown را فراخوانی میکنند، شماره وقتی به صفر میرسد رست نمیشود. همانند مثال فوق، میتوانیم یک CyclicBarrier با شماره 7 تعریف کرده و روی 20 نخ مختلف منتظر آن باشیم.
1ExecutorService es = Executors.newFixedThreadPool(20);
2for (int i = 0; i < 20; i++) {
3 es.execute(() -> {
4 try {
5 if (cyclicBarrier.getNumberWaiting() <= 0) {
6 outputScraper.add("Count Updated");
7 }
8 cyclicBarrier.await();
9 } catch (InterruptedException | BrokenBarrierException e) {
10 // error handling
11 }
12 });
13}
14es.shutdown();
15
16assertTrue(outputScraper.size() > 7);
در این حالت مشاهده میکنیم که مقدار مورد نظر هر بار که نخ جدیدی اجرا میشود کاهش مییابد تا این که با رسیدن به مقدار صفر به مقدار اولیه ریست شود.
سخن پایانی
در نهایت باید اشاره کنیم که هر دو متد CyclicBarrier و CountDownLatch ابزارهای مفیدی برای همگامسازی بین نخهای مختلف هستند. با این حال، تفاوتهای بنیادی بر حسب کارکردهایی که ارائه میکنند دارند. شما باید هر کدام از آنها را به دقت بررسی کنید تا متوجه شوید برای مقصود مورد نظرتان کدام یک مناسبتر است.