CyclicBarrier در جاوا — راهنمای جامع

۲۶ بازدید
آخرین به‌روزرسانی: ۰۵ شهریور ۱۴۰۲
زمان مطالعه: ۵ دقیقه
CyclicBarrier در جاوا — راهنمای جامع

CyclicBarrier-ها سازه‌های همگام‌سازی هستند که به همراه جاوا 5 به عنوان بخشی از پکیج java.util.concurrent معرفی شده‌اند. در این مقاله به بررسی پیاده‌سازی یک سناریوی «هم‌زمانی» (concurrency) با استفاده از CyclicBarrier در جاوا می‌پردازیم.

هم‌زمانی در جاوا - همگام‌سازها

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

  • CyclicBarrier
  • Phaser
  • CountDownLatch
  • Exchanger
  • Semaphore
  • SynchronousQueue

این کلاس‌ها کارکردهای آماده‌ای برای الگوهای تعامل رایج بین نخ‌ها ارائه می‌دهند. اگر یک مجموعه از نخ‌‌ها داشته باشیم که با یکدیگر تعامل دارند و شبیه یکی از الگوهای رایج هستند، می‌توانیم به سادگی از کلاس‌های کتابخانه مناسب که «همگام‌ساز» (Synchronizer) نامیده می‌شوند استفاده مجدد بکنیم و دیگر لازم نیست یک اسکیمای سفارشی با استفاده از مجموعه‌ای قفل و اشیای شرطی و کلیدواژه synchronized بسازیم. در ادامه این مقاله روی CyclicBarrier متمرکز می‌شویم.

CyclicBarrier

CyclicBarrier یک همگام‌ساز است که به ما امکان می‌دهد تا کاری کنیم مجموعه‌ای از نخ‌ها منتظر هم بمانند تا به نقطه اجرای مشترکی برسند. این نقطه مشترک یک «مانع» (Barrier) نامیده می‌شود. CyclicBarrier-ها در برنامه‌هایی استفاده می‌شوند که در آن‌ها تعداد ثابتی نخ داریم و این نخ‌ها باید منتظر هم بمانند تا به نقطه مشترکی در اجرا برسند تا بتوانند کار خود را از سر بگیرند. این «مانع» (Barrier) ‌«چرخه‌ای» (cyclic) نامیده می‌شود، زیرا می‌توان آن را پس از این که نخ‌های منتظر آزاد شدند، مورد استفاده مجدد قرار داد.

کاربرد CyclicBarrier

سازنده یک CyclicBarrier چیز ساده‌ای است. این سازنده از یک عدد صحیح منفرد استفاده می‌کند که نشان‌دهنده تعداد نخ‌هایی است که باید متد ()await روی وهله CyclicBarriers برای نمایش رسیدن به نقطه اجرای مشترک فراخوانی کند:

1public CyclicBarrier(int parties)

نخ‌هایی که باید اجرای خود را همگام‌سازی کنند، «شرکا» (parties)‌ نیز نامیده می‌شوند و با استفاده از فراخوانی متد await()‎ مشخص می‌سازیم که یک نخ به نقطه barrier خاصی رسیده است.

این فراخوانی «همگام» (synchronous) است و نخی که این متد را فراخوانی می‌کند، اجرای خود را تا زمانی که تعداد مشخصی از نخ‌ها همان متد را روی barrier فراخوانی کنند، متوقف می‌کند. این موقعیتی که تعداد مشخصی از نخ‌ها اقدام به فراخوانی ()await کرده باشند، به نام «پرش از مانع» خوانده می‌شود. به صورت اختیاری می‌توان یک آرگومان ثانویه نیز به سازنده ارسال کرد که یک وهله Runnable است. این وهله شامل منطقی است که باید از سوی آخرین نخی که از مانع رد می‌شود، اجرا شود:

1public CyclicBarrier(int parties, Runnable barrierAction)

پیاده‌سازی

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

در ادامه کلاس اصلی را که این کارها در آن رخ می‌دهند، پیاده‌سازی می‌کنیم:

1public class CyclicBarrierDemo {
2 
3    private CyclicBarrier cyclicBarrier;
4    private List<List<Integer>> partialResults
5     = Collections.synchronizedList(new ArrayList<>());
6    private Random random = new Random();
7    private int NUM_PARTIAL_RESULTS;
8    private int NUM_WORKERS;
9 
10    // ...
11}

این کلاس کاملاً سرراست است. NUM_WORKERS تعداد نخ‌هایی است که قرار است اجرا شوند و NUM_PARTIAL_RESULTS تعداد نتایجی است که هر کدام از نخ‌های ورکر قرار است تولید کنند. در نهایت، NUM_PARTIAL_RESULTS را داریم که لیستی است که قرار است نتایج هر یک از این نخ‌های ورکر در آن ذخیره شوند. توجه کنید که این لیست یک NUM_PARTIAL_RESULTS است، زیرا چندین نخ هم‌زمان منتظر هستند و متد add روی یک ArrayList به صورت thread-safe نیست.

اکنون منطق هر کدام از نخ‌های ورکر را پیاده‌سازی می‌کنیم:

1public class CyclicBarrierDemo {
2 
3    // ...
4 
5    class NumberCruncherThread implements Runnable {
6 
7        @Override
8        public void run() {
9            String thisThreadName = Thread.currentThread().getName();
10            List<Integer> partialResult = new ArrayList<>();
11 
12            // Crunch some numbers and store the partial result
13            for (int i = 0; i < NUM_PARTIAL_RESULTS; i++) {    
14                Integer num = random.nextInt(10);
15                System.out.println(thisThreadName
16                  + ": Crunching some numbers! Final result - " + num);
17                partialResult.add(num);
18            }
19 
20            partialResults.add(partialResult);
21            try {
22                System.out.println(thisThreadName 
23                  + " waiting for others to reach barrier.");
24                cyclicBarrier.await();
25            } catch (InterruptedException e) {
26                // ...
27            } catch (BrokenBarrierException e) {
28                // ...
29            }
30        }
31    }
32 
33}

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

1public class CyclicBarrierDemo {
2 
3    // ...
4     
5    class AggregatorThread implements Runnable {
6 
7        @Override
8        public void run() {
9 
10            String thisThreadName = Thread.currentThread().getName();
11 
12            System.out.println(
13              thisThreadName + ": Computing sum of " + NUM_WORKERS 
14              + " workers, having " + NUM_PARTIAL_RESULTS + " results each.");
15            int sum = 0;
16 
17            for (List<Integer> threadResult : partialResults) {
18                System.out.print("Adding ");
19                for (Integer partialResult : threadResult) {
20                    System.out.print(partialResult+" ");
21                    sum += partialResult;
22                }
23                System.out.println();
24            }
25            System.out.println(thisThreadName + ": Final result = " + sum);
26        }
27    }
28}

گام آخر، ساخت CyclicBarrier است که با متد ()main آغاز می‌شود:

1public class CyclicBarrierDemo {
2 
3    // Previous code
4  
5    public void runSimulation(int numWorkers, int numberOfPartialResults) {
6        NUM_PARTIAL_RESULTS = numberOfPartialResults;
7        NUM_WORKERS = numWorkers;
8 
9        cyclicBarrier = new CyclicBarrier(NUM_WORKERS, new AggregatorThread());
10 
11        System.out.println("Spawning " + NUM_WORKERS
12          + " worker threads to compute "
13          + NUM_PARTIAL_RESULTS + " partial results each");
14  
15        for (int i = 0; i < NUM_WORKERS; i++) {
16            Thread worker = new Thread(new NumberCruncherThread());
17            worker.setName("Thread " + i);
18            worker.start();
19        }
20    }
21 
22    public static void main(String[] args) {
23        CyclicBarrierDemo demo = new CyclicBarrierDemo();
24        demo.runSimulation(5, 3);
25    }
26}

در کد فوق، یک «مانع چرخه‌ای» با 5 نخ مقداردهی می‌کنیم که هر کدام در طی محاسباتشان 3 عدد صحیح تولید و آن‌ها‌ را در لیست حاصل ذخیره می‌کنند. زمانی که مانع رد می‌شود، آخرین نخ که از مانع رد شود، منطق مشخص‌شده در AggregatorThread را اجرا می‌کند و همه اعداد تولیدشده از سوی نخ‌ها را جمع می‌زند.

نتایج

خروجی یک بار اجرای برنامه فوق به صورت زیر است. هر اجرا می‌تواند نتایج متفاوتی ارائه دهد، زیرا نخ‌ها می‌توانند با ترتیب‌های متفاوتی اجرا شوند:

Spawning 5 worker threads to compute 3 partial results each
Thread 0: Crunching some numbers! Final result - 6
Thread 0: Crunching some numbers! Final result - 2
Thread 0: Crunching some numbers! Final result - 2
Thread 0 waiting for others to reach barrier.
Thread 1: Crunching some numbers! Final result - 2
Thread 1: Crunching some numbers! Final result - 0
Thread 1: Crunching some numbers! Final result - 5
Thread 1 waiting for others to reach barrier.
Thread 3: Crunching some numbers! Final result - 6
Thread 3: Crunching some numbers! Final result - 4
Thread 3: Crunching some numbers! Final result - 0
Thread 3 waiting for others to reach barrier.
Thread 2: Crunching some numbers! Final result - 1
Thread 2: Crunching some numbers! Final result - 1
Thread 2: Crunching some numbers! Final result - 0
Thread 2 waiting for others to reach barrier.
Thread 4: Crunching some numbers! Final result - 9
Thread 4: Crunching some numbers! Final result - 3
Thread 4: Crunching some numbers! Final result - 5
Thread 4 waiting for others to reach barrier.
Thread 4: Computing final sum of 5 workers, having 3 results each.
Adding 6 2 2
Adding 2 0 5
Adding 6 4 0
Adding 1 1 0
Adding 9 3 5
Thread 4: Final result = 46

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

سخن پایانی

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

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

==

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
baeldung
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *