পাঠ ১৯ · ৫৬-এর মধ্যে · মডিউল ৫
Home / Courses / Operating Systems (OS) / প্রডিউসার-কনজিউমার

ক্লাসিক সিনক্রোনাইজেশন: প্রডিউসার-কনজিউমার

Classic synchronization: producer-consumer
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • বাউন্ডেড-বাফার প্রডিউসার-কনজিউমার সমস্যার সংজ্ঞা এবং কেন এটি সিঙ্গেল-মিউটেক্স সমাধানের চেয়ে বেশি জটিল
  • কেন ঠিক তিনটি সেমাফোর দরকার — empty, full, এবং mutex — এবং প্রতিটির নির্দিষ্ট ভূমিকা
  • সঠিক wait/signal অর্ডারিং — এবং ভুল অর্ডার কীভাবে ডেডলক বা করাপশন ঘটাতে পারে
  • L18-এর Semaphore ক্লাস পুনরায় ব্যবহার করে সম্পূর্ণ produce()/consume() ফাংশন লিখে ব্লকিং ও আনব্লকিং সরাসরি ট্রেস করা

১ · বাউন্ডেড-বাফার সমস্যা

প্রডিউসার-কনজিউমার সমস্যাProducer-Consumer Problemপ্রডিউসাররা আইটেম তৈরি করে একটি ফিক্সড-সাইজ শেয়ার্ড বাফারে রাখে, কনজিউমাররা সেখান থেকে আইটেম সরিয়ে প্রসেস করে। -তে একদল প্রডিউসার আইটেম তৈরি করে একটি বাউন্ডেড বাফারBounded Bufferএকটি ফিক্সড-সাইজ শেয়ার্ড ডেটা স্ট্রাকচার -- সীমিত সংখ্যক স্লট আছে।-এ রাখে, আর একদল কনজিউমার সেখান থেকে আইটেম সরিয়ে প্রসেস করে। সমস্যাটা দুই দিক থেকেই আসে — বাফার পূর্ণ থাকলে প্রডিউসারকে থামতে হবে (নাহলে ওভাররাইট/ওভারফ্লো), আর বাফার খালি থাকলে কনজিউমারকে থামতে হবে (নাহলে অস্তিত্বহীন আইটেম প্রসেস করার চেষ্টা)। এটি L18-এর একক মিউটেক্সের চেয়ে বেশি জটিল, কারণ শুধু "একসাথে একজন" নয় — এখানে "কতগুলো স্লট ফাঁকা/ভরা আছে" তাও ট্র্যাক করতে হয়।

২ · তিনটি সেমাফোর কেন লাগে

empty
কাউন্টিং সেমাফোর, শুরুতে buffer_size-এ ইনিশিয়ালাইজড — কতগুলো ফাঁকা স্লট আছে তা গোনে। প্রডিউসার নতুন আইটেম বসানোর আগে এতে wait করে।
full
কাউন্টিং সেমাফোর, শুরুতে ০-তে ইনিশিয়ালাইজড — কতগুলো ভরা স্লট আছে তা গোনে। কনজিউমার আইটেম সরানোর আগে এতে wait করে।
mutex
বাইনারি সেমাফোর (L18-এর মতো), শুধু বাফারের প্রকৃত read/write অপারেশনটুকু প্রোটেক্ট করে — যাতে দুজন একসাথে বাফার স্পর্শ না করে।
Producer empty.wait() Bounded Buffer [ ][ ][ ] mutex দিয়ে প্রোটেক্টেড full.wait() Consumer full.signal() empty.signal()
প্রডিউসার empty-তে wait করে বাফারে রাখে ও full সিগন্যাল করে; কনজিউমার ঠিক উল্টো — full-এ wait করে বাফার থেকে সরায় ও empty সিগন্যাল করে।
অর্ডারিং কেন গুরুত্বপূর্ণ

একটি প্রডিউসারকে অবশ্যই এই ক্রমে কাজ করতে হবে — empty.wait() → mutex.wait() → বাফারে যোগ → mutex.signal() → full.signal()। যদি mutex.wait() আগে আর empty.wait() পরে করা হতো, একটি পূর্ণ বাফারে প্রডিউসার mutex ধরে রেখেই empty-তে ব্লক হয়ে যেত — আর কেউ কখনো mutex রিলিজ করতে পারত না, যেহেতু মিউটেক্স ধরে রাখা অবস্থাতেই সে আটকে গেছে। এটি একটি ক্লাসিক ডেডলক সিনারিও — অর্ডার একটু ভুল হলেই যা ঘটতে পারে।

৩ · কোডে সম্পূর্ণ সমাধান — সাইজ-২ বাফার

নিচে L18-এর Semaphore ক্লাস অপরিবর্তিতভাবে পুনরায় ব্যবহার করে produce() ও consume() লেখা হলো। যেহেতু এটি একটি সিকোয়েন্সিয়াল সিমুলেশন (কোনো real থ্রেড ব্লক হয়ে "পরে আবার নিজে থেকে জেগে ওঠে না"), তাই যখন কোনো প্রডিউসার/কনজিউমার ব্লক হয়, আমরা স্পষ্টভাবে দেখাই যে পরবর্তী সংশ্লিষ্ট signal()-ই তাকে জাগিয়ে তার বাকি কাজ (মিউটেক্স নেওয়া, বাফার ছোঁয়া, রিলিজ করা) সম্পন্ন করায় — ঠিক যেভাবে বাস্তব সেমাফোরে একটি জাগ্রত প্রসেস তার wait() কলের ঠিক পর থেকেই আবার চলা শুরু করে।

Python
class Semaphore:  # L18-এর ক্লাস, অপরিবর্তিত
    def __init__(self, initial_value):
        self.value = initial_value
        self.waiting = []

    def wait(self, name):
        self.value -= 1
        if self.value < 0:
            self.waiting.append(name)
            print(f"    {name}: wait() -> value={self.value} (BLOCKED, waiting={self.waiting})")
            return False
        print(f"    {name}: wait() -> value={self.value} (proceed)")
        return True

    def signal(self, name):
        self.value += 1
        woken = None
        if self.value <= 0 and self.waiting:
            woken = self.waiting.pop(0)
            print(f"    {name}: signal() -> value={self.value} (wakes {woken})")
        else:
            print(f"    {name}: signal() -> value={self.value}")
        return woken


BUFFER_SIZE = 2
buffer = []

empty = Semaphore(BUFFER_SIZE)  # ফাঁকা স্লট
full = Semaphore(0)             # ভরা স্লট
mutex = Semaphore(1)            # বাফার প্রোটেকশন

pending_item = [None]  # ব্লকড হওয়া producer-এর আইটেম মনে রাখার জন্য

def produce(item):
    print(f"\nProducer -> '{item}' produce করার চেষ্টা")
    ok = empty.wait("Producer")
    if not ok:
        pending_item[0] = item
        print(f"  Producer blocked -- বাফার পূর্ণ, '{item}' পেন্ডিং")
        return
    mutex.wait("Producer")
    buffer.append(item)
    print(f"  বাফারে যোগ হলো: {buffer}")
    mutex.signal("Producer")
    woken = full.signal("Producer")
    if woken:
        resume_consumer_after_wakeup()

def resume_consumer_after_wakeup():
    print("  [জাগ্রত হওয়া Consumer তার বাকি কাজ সম্পন্ন করছে]")
    mutex.wait("Consumer(resume)")
    item = buffer.pop(0)
    print(f"    বাফার থেকে সরানো হলো: {item}, বাফার এখন: {buffer}")
    mutex.signal("Consumer(resume)")
    empty.signal("Consumer(resume)")

def consume():
    print("\nConsumer -> আইটেম consume করার চেষ্টা")
    ok = full.wait("Consumer")
    if not ok:
        print("  Consumer blocked -- বাফার খালি")
        return
    mutex.wait("Consumer")
    item = buffer.pop(0)
    print(f"  বাফার থেকে সরানো হলো: {item}, বাফার এখন: {buffer}")
    mutex.signal("Consumer")
    woken = empty.signal("Consumer")
    if woken:
        resume_producer_after_wakeup()

def resume_producer_after_wakeup():
    print("  [জাগ্রত হওয়া Producer তার বাকি কাজ সম্পন্ন করছে]")
    item = pending_item[0]
    mutex.wait("Producer(resume)")
    buffer.append(item)
    print(f"    বাফারে যোগ হলো: {buffer}")
    mutex.signal("Producer(resume)")
    full.signal("Producer(resume)")


produce("A")               # খালি বাফার -> সরাসরি সফল
produce("B")                # বাফার এখন পূর্ণ (সাইজ ২)
produce("C")                 # বাফার পূর্ণ -> BLOCKED
consume()                    # A সরায়, এবং ব্লকড Producer("C") কে জাগায়
consume()                    # B সরায়
consume()                    # C সরায় (এখন বাফার আবার খালি)
consume()                    # বাফার খালি -> BLOCKED
produce("D")                  # ব্লকড Consumer কে জাগায়

print("\nফাইনাল বাফার:", buffer)
print("empty.value:", empty.value, " full.value:", full.value, " mutex.value:", mutex.value)

    
লক্ষ্য করুন দুটো ব্লকিং সিনারিওই — produce("C") বাফার পূর্ণ থাকায় ব্লক হয়, এবং পরের consume() কল সেই ব্লকড প্রডিউসারকে জাগিয়ে তার আইটেম বাফারে বসায়। একইভাবে চতুর্থ consume() বাফার খালি থাকায় ব্লক হয়, আর produce("D") তাকে জাগায়। উভয় ক্ষেত্রেই mutex নিশ্চিত করে বাফার অ্যাক্সেস কখনো একসাথে দুজনের হাতে যায় না।
মূল কথা · Key takeaway

empty ও full একে অপরের পরিপূরক (তাদের যোগফল সবসময় buffer_size মাইনাস যতজন এই মুহূর্তে ব্লকড আছে তার সমান থাকে), আর mutex শুধু বাফারের প্রকৃত টাচ-পয়েন্টটুকু সুরক্ষিত রাখে — বাকি সবকিছু (কে কখন এগোতে পারবে) দুটো কাউন্টিং সেমাফোরের মধ্যে ভাগ করা। এই তিন-সেমাফোর প্যাটার্ন এতটাই মৌলিক যে L20-এর readers-writers সমস্যাতেও একই ধরনের mutex + রিসোর্স-লক কম্বিনেশন ব্যবহৃত হবে।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ যদি produce() ফাংশনে empty.wait() ও full.signal()-এর মাঝে mutex.wait()/mutex.signal() না থাকত (শুধু buffer.append() সরাসরি করা হতো), তাহলে কী সমস্যা হতে পারত?

যদি একই মুহূর্তে দুইজন প্রডিউসার (উভয়েরই empty-তে যথেষ্ট মান আছে) একসাথে buffer.append() কল করার চেষ্টা করত, তাহলে এটি ঠিক L17-এর মতো একটি রেস কন্ডিশন হতো — বাফারের অভ্যন্তরীণ ডেটা স্ট্রাকচার (list-এর length, পয়েন্টার ইত্যাদি) একসাথে দুইজনের হাতে পরিবর্তিত হলে করাপ্ট হয়ে যেতে পারত। empty ও full শুধু "কতজন এগোতে পারবে" নিয়ন্ত্রণ করে, কিন্তু বাফারের প্রকৃত read/write নিজে প্রোটেক্ট করে না — সেই কাজটা একান্তভাবে mutex-এর।

প্র ০২ উপরের কোড সেলে produce("C") ব্লক হওয়ার পর empty.value কত হয়? সেই মানটা কী বোঝায়?

produce("C") ব্লক হওয়ার পর empty.value = -1 হয়। এর মানে একজন প্রসেস (Producer, "C" আইটেম নিয়ে) ফাঁকা স্লটের জন্য অপেক্ষা করছে। যখনই কোনো consumer একটি আইটেম সরিয়ে empty.signal() কল করবে, value -1 থেকে 0 হবে এবং যেহেতু waiting list-এ কেউ আছে, তাকে জাগানো হবে — ঠিক যেমনটা পরের consume() কলে ঘটেছে।

প্র ০৩ এই সমাধানে যদি একাধিক প্রডিউসার ও একাধিক কনজিউমার একসাথে থাকত (শুধু ১টি করে নয়), তাহলে কি কোনো অতিরিক্ত পরিবর্তন দরকার হতো?

তিনটি সেমাফোরের মূল লজিক অপরিবর্তিত থাকত, কারণ empty/full/mutex ইতিমধ্যেই "কতজন" ধারণাটি সঠিকভাবে সামলায় (নির্দিষ্ট কোনো একজন প্রডিউসার/কনজিউমারের ওপর নির্ভর করে না)। তবে বাস্তব ব্যবহারে যদি একাধিক প্রডিউসার একসাথে empty.wait() পার হয়ে যায়, তাদের নিজেদের মধ্যেও বাফারে-লেখার ক্রম ঠিক রাখতে mutex-ই যথেষ্ট (যেহেতু mutex সব প্রডিউসার ও কনজিউমারের জন্য একটাই শেয়ার্ড লক) — আলাদা কোনো নতুন সেমাফোর দরকার নেই।

অনুশীলন

  1. পরীক্ষা করুন: উপরের কোড সেলে BUFFER_SIZE = 2-কে BUFFER_SIZE = 3 করে Run চাপুন — এবার কি produce("C") ব্লক হয়?

    না, বাফার সাইজ ৩ করলে empty শুরু হবে ৩ দিয়ে — তিনটি produce() কলই (A, B, C) সরাসরি সফল হবে, কেউ ব্লক হবে না। ব্লকিং তখনই ঘটে যখন প্রডিউসারদের সংখ্যা বাফারের ফাঁকা স্লটের চেয়ে বেশি হয়ে যায় — তাই সাইজ বাড়ালে একই সংখ্যক produce() কলে ব্লকিং কম দেখা যাবে।

  2. চিন্তা করুন: যদি কোডে ভুলবশত প্রডিউসারের empty.wait() ও full.signal()-এর অর্ডার উল্টে ফেলা হতো (আগে full.signal, পরে empty.wait), তাহলে কী সমস্যা হতো?

    এই অর্ডার উল্টালে প্রতিটি produce() কল আসলে বাফারে আইটেম বসানোর আগেই full সিগন্যাল করে ফেলত — একজন consumer তখন এমন একটি স্লট থেকে আইটেম "সরানোর" চেষ্টা করতে পারত যা আসলে এখনো বসানো হয়নি (অথবা এখনো ফাঁকা), যা ডেটা করাপশন বা IndexError-এর মতো সমস্যা তৈরি করত। এটিই ব্রিফে উল্লেখিত সতর্কতা — wait/signal-এর সঠিক অর্ডার মানা বাধ্যতামূলক, নাহলে সমাধানটি হয় ডেডলক করে অথবা ডেটা করাপ্ট করে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
মিউটেক্স লক ও সেমাফোর