بررسی مسئله ی Producer-Consumer در جاوا

java-multithreading-Producer-Consumer

سلام در قسمت های قبلی آموزش با اصول مقدماتی برنامه نویسی  Multithreading در جاوا آشنا شدیم و چند مثال ساده و ابتدایی بررسی شد در این قسمت از آموزش قصد داریم یک مثال جامع و البته معروف در زمینه ی برنامه نویسی Multithreading را به صورت کامل بررسی کنیم.

مسئله ی Producer-Consumer چیست؟

اصول کلی این مسئله این است که یک یا چند Thread، یک نوع داده ای را تولید و یک یا چند Thread، آن داده ها را پردازش (مصرف) می کنند.

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

لازم به ذکر است که در این مسئله تعداد تولید کننده و تعداد مصرف کننده یا ظرفیت مخزن می تواند محدود باشد.مثلا به شکل زیر توجه کنید در اینجا سه تولید کننده و پنج مصرف کننده وجود دارد .

تولید کننده - مصرف کننده یا Producer-Consumer در جاوا
تولید کننده - مصرف کننده یا Producer-Consumer

قبل از بیان این مسئله ابتدا لازم است چند نکته یادآوری شود:

1- دو Thread همزمان نباید با مخزن (شی مشترک) کار کنند، مثلا اگر یک Thread در حال تولید داده است، Thread مصرف کننده نباید به داده ها دسترسی داشته باشد تا زمانی که کار تولید کننده به پایان برسد یا به عبارت دیگری اگر یک Thread در حال خواندن یا نوشتن بر روی مخزن است، Thread دیگری نباید مزاحم شود.

2- اگر مخزن مشترک خالی باشد، Thread مصرف کننده باید منتظر بماند (wait) تا Thread تولید کننده، داده ای جدید تولید کند (notify) و به همین ترتیب اگر طول مخزن محدود فرض شود و مخزن پر باشد Thread تولید کننده نباید داده ای تولید کند (wait) تا Thread مصرف کننده شروع به پردازش داده های مخزن کند (استفاده) و بلافاصله با آزاد شدن یک مکان از مخزن تولید کننده دوباره فعال شود.

خب در این مثال یک حالت اولیه و ساده را بررسی می کنیم که دو تولید کننده و دو مصرف کننده داریم و مخزن هم نامحدود است.

کد برنامه ی Producer-Consumer به صورت زیر است، آن را در Netbeans یا eclipse پیاده سازی کنید تا به بررسی آن بپردازیم.

public class ProducerConsumer {
    public static void main(String[] args) {
        List<Integer> list = new LinkedList<>();
        Thread[] threads = {new Producer(list),new Producer(list),
        new Consumer(list),new Consumer(list)};
        threads[0].setName("Producer 1");
        threads[1].setName("Producer 2");
        threads[2].setName("Consumer 1");
        threads[3].setName("Consumer 2");
        for (Thread thread : threads) {
            thread.start();
        }
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException ex) {}
        }
        System.out.println("Finished: size="+list.size());
    }
}
class Producer extends  Thread{
    List<Integer> list;

    public Producer(List<Integer> list) {
       this.list=list;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized(list){
                Integer num = (int)(Math.random()*100);
                System.out.println("Thread name = "+Thread.currentThread().getName()+"\t"+"Added :"+num);
                list.add(num);
                list.notify();
            }
            try {
                Thread.sleep(100);
            } catch (Exception e) {}    
        }
    } 
}
class Consumer extends Thread{
    List<Integer> list;

    public Consumer(List<Integer> list) {
        this.list=list;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized(list){
                while (list.size()==0) {
                    try {
                        list.wait();
                    } catch (Exception e) {}
                }
                Integer delete = list.remove(0);
                System.out.println("Thread name = "+Thread.currentThread().getName()+"\t"+"Removed :"+delete);
            }  
        }
    } 
}

مثال را با توضیح کلاس Main شروع می کنیم.

در اینجا مخزن مشترک را ساختمان داده List در نظر گرفته ایم، در خط بعدی در یک آرایه از جنس Thread چهار عدد نخ تولید کرده ایم که 2 تا تولید کننده و 2 تا مصرف کننده هستند. لازم به ذکر است ورودی سازنده ی هر دو کلاس شی از جنس list می باشد، در ادامه برای Thread ها نامی مناسب در نظر گرفته شده است. در خط بعدی با استفاده از foreach هر چهار Thread به ترتیب start شده اند و در foreach بعدی متد join برای هر چهار Thread فراخوانی شده تا هر Thread تا پایان کار Thread دیگر منتظر بماند. در پایان هم طول list چاپ می شود.

کلاس Producer: در این کلاس ابتدا یک شی از List ایجاد و در سازنده ی کلاس مقدار دهی شده است، در متد run یک حلقه با تکرار 10 وجود دارد که شی list در داخل آن در بلوک synchronized استفاده شده است، بدیهی است که شی مشترک باید به گونه ای سازماندهی شود که در هر لحظه فقط توسط یک نخ قابل دستیابی باشد. در داخل بلوک به صورت random یک عدد تولید و در متغییر num که از نوع Integer است ذخیره می شود. در خط بعدی نام Thread که این عملیات را انجام داده همراه با مقدار آن عدد چاپ می شود.

در ادامه، آن عدد به list اضافه می شود و در پایان متد notify بر روی شی مشترک فراخوانی می شود تا اگر یک Thread بر روی آن شی wait کرده بیدار و فعال شود.

متد sleep استفاده شده در این کلاس فقط برای دیدن روند اجرای برنامه و تغییرات خروجی بوده و هیچ کاربرد دیگری ندارد و می تواند حذف شود.

کلاس Consumer: دقیقا مانند کلاس Producer شی مشترک تعریف و در سازنده مقدار دهی شده است در این کلاس هر آنچه که تولید کننده، تولید کرده و در list قرار داده، حذف می شود. یک for ده تایی دقیقا مطابق با مقدار شمارنده for در کلاس Producer است و مانند قبل، شی list در بلوک synchronized قرار داده شده است. توجه داشته باشید که این کلاس مصرف کننده می باشد. بنابراین باید عنصری قبلا توسط تولید کننده، بوجود آمده باشد تا در این کلاس استفاده شود. در حلقه ی while تا زمانی که طول List صفر باشد متد wait بر روی شی فراخوانی خواهد شد تا وقتی که یک Thread تولید کننده بر روی شی مشترک notify کند و اعلام کند که داده ای در list موجود است. بنابراین روند اجرای برنامه از while خارج می شود و همان عنصری که توسط تولید کننده، تولید شده دقیقا همان مصرف می شود (list.remove(0)) و در پایان نام Thread که این عملیات را انجام داده همراه با عنصری که حذف شده در خروجی چاپ می شود.

خب با توجه به توضیحات بالا چون هر عنصری که تولید می شود در مصرف کننده استفاده می شود، پس بدیهی است که باید طول شی list در پایان، صفر شود.

توجه داشته باشید هر Thread برای کار با شی باید قفل آن را در اختیار بگیرد تا Thread های دیگر اجازه ی ورود به آن و تغییرات را نداشته باشد پس این شی در بلوک synchronized استفاده شده است.

خروجی را در سیستم خود مشاهده کنید و به دقت به اعداد اضافه شده و حذف شده و Thread ی که این عملیات را انجام داده توجه کنید، خواهید دید که هر Thread دقیقا 10 بار اجرا شده است.

این مثال یک حالت بسیار ساده و ابتدایی از Producer-Consumer بود. پس برای درک بیشتر این مسئله، آن را در حالت های مختلف اجرا کنید، مثلا notify یا wait یا هر دو را حذف کنید و یا شی list از بلوک synchronized را خارج کرده و خروجی را مشاهد نمایید و Error های آن را بررسی کرده تا درک هر بخش راحت تر شود چون تنها راه یادگیری عمیق Multithreading تمرین و حل مثال های بسیار است.

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

موفق باشید.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش برنامه نویسی MultiThreading در جاوا توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.