পাঠ ২২ · ৫৬-এর মধ্যে · মডিউল ৫
Home / Courses / Operating Systems (OS) / মনিটর

মনিটর ও উচ্চ-স্তরের সিনক্রোনাইজেশন কনস্ট্রাক্ট

Monitors & high-level synchronization constructs
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • সেমাফোর-ভিত্তিক সমাধান কেন বাস্তব বড় কোডবেসে বাগ-প্রবণ, তার সুনির্দিষ্ট কারণ
  • মনিটর কী, এটি কীভাবে মিউটেক্স স্বয়ংক্রিয়ভাবে বলবৎ করে, এবং কন্ডিশন ভেরিয়েবল কীভাবে কাজ করে
  • বাস্তব ভাষায় মনিটরের প্রয়োগ — Java synchronized, Python threading.Condition
  • Python-এ একটি সরল "মনিটর-স্টাইল" bounded buffer ক্লাস বানিয়ে L19-এর ম্যানুয়াল সেমাফোর সমাধানের সাথে সরাসরি তুলনা

১ · সেমাফোরের আসল সমস্যাটা কী

L18-L21-এ আমরা সেমাফোর দিয়ে ক্রিটিক্যাল সেকশন, প্রডিউসার-কনজিউমার, রিডার্স-রাইটার্স ও ডাইনিং ফিলোসফার্স সমাধান করেছি। প্রতিটি ক্ষেত্রেই সমাধান কাজ করেছে — কিন্তু একটি লুকানো শর্তে: প্রোগ্রামারকে প্রতিটি শেয়ার্ড ডেটা অ্যাক্সেসের আগে-পরে সঠিক ক্রমে ঠিক wait()/signal() কল বসাতে হয়েছে। একটি বড় কোডবেসে, যেখানে শত শত জায়গায় শেয়ার্ড ডেটা অ্যাক্সেস হয়, একটিমাত্র ভুলে-বসানো বা ভুলে-যাওয়া কল —

  • একটি signal() ভুলে বাদ পড়লে অন্য সব প্রসেস চিরকালের জন্য আটকে যেতে পারে,
  • দুটি wait() ভুল ক্রমে বসালে ডেডলক হতে পারে (M6-এ বিস্তারিত),
  • কোনো একটি অ্যাক্সেস পয়েন্টে wait()/signal() বসাতেই ভুলে গেলে চুপচাপ একটি রেস কন্ডিশন থেকে যায় — কম্পাইলার বা রানটাইম কোনো সতর্কতা দেয় না।
এটি একটি সত্যিকারের, সুপরিচিত সফটওয়্যার-ইঞ্জিনিয়ারিং সমস্যা — সেমাফোর নিজে "ভুল" নয়, কিন্তু এটি সঠিকতার পুরো দায়ভার প্রোগ্রামারের হাতে ছেড়ে দেয়, কোনো ভাষা-স্তরের সুরক্ষা ছাড়াই।

২ · মনিটর কী

মনিটর (Monitor)Monitorএকটি উচ্চ-স্তরের সিনক্রোনাইজেশন কনস্ট্রাক্ট যা শেয়ার্ড ডেটা ও তার উপর কাজ করা প্রসিডিওরগুলোকে একসাথে বান্ডেল করে, এবং নিশ্চিত করে একসময়ে কেবল একটি প্রসেস/থ্রেড মনিটরের ভেতরে সক্রিয় থাকতে পারবে। হলো একটি প্রোগ্রামিং-ভাষা-সমর্থিত কনস্ট্রাক্ট যা শেয়ার্ড ডেটা এবং সেই ডেটার উপর কাজ করা প্রসিডিওরগুলোকে একসাথে একটি একক ইউনিটে আবদ্ধ করে। মূল গ্যারান্টি: একসময়ে কেবলমাত্র একটি প্রসেস/থ্রেড মনিটরের ভেতরে সক্রিয়ভাবে চলতে পারবে — এই মিউচুয়াল এক্সক্লুশন প্রোগ্রামার নিজে বসায় না, ভাষা বা রানটাইম নিজেই স্বয়ংক্রিয়ভাবে বলবৎ করে। ফলে সেমাফোরের মতো "প্রতিটি জায়গায় সঠিক ক্রমে wait/signal বসিয়েছি তো?" প্রশ্নটাই আর থাকে না।

প্রসেস P1 প্রসেস P2 প্রসেস P3 মনিটরের সীমানা — এখানে একসাথে কেবল একজন ঢুকতে পারবে শেয়ার্ড ডেটা + procedures (মনিটরের ভেতরে) কন্ডিশন ভেরিয়েবল (প্রয়োজনে অপেক্ষা)
P1, P2, P3 — সবাই মনিটরে ঢুকতে চাইলেও, রানটাইম নিশ্চিত করে একসময়ে একজনই ভেতরে সক্রিয় থাকবে; বাকিরা বাইরে অপেক্ষা করে।

৩ · কন্ডিশন ভেরিয়েবল

মনিটরের ভেতরে ঢুকেই যদি কোনো প্রসেস দেখে তার দরকারি শর্ত এখনো সত্যি নয় (যেমন বাফার খালি, তাই consume করার কিছু নেই), তাকে কিছু একটা করতে হবে — কিন্তু মনিটরের ভেতরে বসেই অপেক্ষা করলে অন্য কেউ (যে ঐ শর্ত পূরণ করতে পারত) মনিটরে ঢুকতেই পারবে না, ফলে ডেডলক। এই সমস্যার সমাধান কন্ডিশন ভেরিয়েবল (Condition Variable)Condition Variableমনিটরের ভেতরে থাকা একটি প্রসেসকে মনিটরের লক সাময়িকভাবে ছেড়ে দিয়ে অপেক্ষায় পাঠানোর প্রক্রিয়া (wait), এবং শর্ত পূরণ হলে কাউকে জাগানোর প্রক্রিয়া (signal) — সেমাফোরের wait()/signal() থেকে ভিন্ন একটি মেকানিজম। — এটি একটি প্রসেসকে মনিটরের লক সাময়িকভাবে ছেড়ে দিয়ে অপেক্ষায় পাঠাতে দেয় (wait()), যাতে অন্যরা ভেতরে ঢুকে শর্তটি পূরণ করতে পারে; শর্ত পূরণ হলে অপেক্ষমাণ কাউকে জাগানো হয় (signal()/ notify()), যে তখন লক পুনরায় নিয়ে চালিয়ে যায়। এটি সেমাফোরের wait()/signal() থেকে ভিন্ন — সেমাফোরের wait() মনিটরের কোনো লক ছাড়ে না, কারণ সেমাফোরে "মনিটর" ধারণাটাই নেই।

Java — synchronized + wait()/notify()
একটি মেথড বা ব্লককে synchronized চিহ্নিত করলে JVM নিজেই সেই অবজেক্টের লক অ্যাকোয়ার/রিলিজ করে; ভেতরে wait()/notify() কন্ডিশন ভেরিয়েবলের কাজ করে।
Python — threading.Condition
একটি অন্তর্নিহিত লকসহ কন্ডিশন ভেরিয়েবল দেয় — with cond: ব্লকে ঢুকে cond.wait()/cond.notify() কল করা যায়, লক ম্যানেজমেন্ট স্বয়ংক্রিয়।

৪ · একটি মনিটর-স্টাইল bounded buffer

নিচে L19-এর 3-সেমাফোর প্রডিউসার-কনজিউমার সমাধানের বিপরীতে একটি "মনিটর-স্টাইল" bounded buffer দেখানো হলো — এখানেও বাস্তব থ্রেড নয়, শুধুই একটি ইন-মেমরি লক-সিমুলেশন (SimulatedLock) যা দেখায় মনিটরের মূল ধারণা: caller নিজে কখনো লক অ্যাকোয়ার/রিলিজ করছে না — একটি ডেকোরেটর প্রতিটি পাবলিক মেথডের শুরুতে-শেষে স্বয়ংক্রিয়ভাবে তা করছে।

Python
# সিমুলেটেড মনিটর -- বাস্তব থ্রেড/লক নয়, শুধুই একটি ইন-মেমরি লক-সিমুলেশন ধারণা বোঝানোর জন্য

class SimulatedLock:
    def __init__(self):
        self.locked = False

    def acquire(self):
        if self.locked:
            raise RuntimeError("লক ইতিমধ্যে ধরা আছে -- মনিটরের ভেতরে একসাথে দুইজন ঢুকতে পারবে না")
        self.locked = True

    def release(self):
        self.locked = False


def synchronized(method):
    """ডেকোরেটর -- মেথড কল হওয়ার আগে স্বয়ংক্রিয়ভাবে লক অ্যাকোয়ার করে, শেষে রিলিজ করে।
    caller-কে ম্যানুয়ালি wait()/signal() লিখতে হয় না -- এটাই মনিটরের মূল সুবিধা।"""
    def wrapper(self, *args, **kwargs):
        self._lock.acquire()
        try:
            result = method(self, *args, **kwargs)
        finally:
            self._lock.release()
        return result
    return wrapper


class MonitorBoundedBuffer:
    """L19-এর মতোই একটি bounded buffer -- কিন্তু caller-কে মিউটেক্স/সেমাফোর
    ম্যানুয়ালি সামলাতে হয় না; মনিটর (এই ক্লাস) নিজেই তা সামলায়।"""
    def __init__(self, capacity):
        self.capacity = capacity
        self.buffer = []
        self._lock = SimulatedLock()

    @synchronized
    def produce(self, item):
        if len(self.buffer) >= self.capacity:
            return f"বাফার পূর্ণ -> {item} প্রত্যাখ্যাত (বাস্তব মনিটরে এখানে condition variable-এ wait() করা হতো)"
        self.buffer.append(item)
        return f"{item} যোগ হলো -> বাফার এখন {self.buffer}"

    @synchronized
    def consume(self):
        if not self.buffer:
            return "বাফার খালি -> consume করার কিছু নেই (বাস্তব মনিটরে এখানে wait() করা হতো)"
        item = self.buffer.pop(0)
        return f"{item} নেওয়া হলো -> বাফার এখন {self.buffer}"


buf = MonitorBoundedBuffer(capacity=2)
print(buf.produce("A"))
print(buf.produce("B"))
print(buf.produce("C"))   # পূর্ণ -- caller কোনো সেমাফোর ছাড়াই নিরাপদ প্রত্যাখ্যান পেলো
print(buf.consume())
print(buf.produce("C"))   # এখন জায়গা খালি হয়েছে
print(buf.consume())
print(buf.consume())
print(buf.consume())      # খালি

print("\n--- রিএন্ট্র্যান্সি সমস্যা (একটি বাস্তব মনিটর-সাবধানতা) ---")

class BuggyMonitor(MonitorBoundedBuffer):
    @synchronized
    def produce_two(self, item1, item2):
        # ভুল করে একই অবজেক্টের আরেকটি synchronized মেথড ভেতর থেকে কল করা হলো
        self.produce(item1)
        self.produce(item2)

buggy = BuggyMonitor(capacity=5)
try:
    buggy.produce_two("X", "Y")
except RuntimeError as e:
    print("ত্রুটি ধরা পড়লো:", e)

    
লক্ষ্য করুন — produce()/consume() কল করার সময় caller একবারও lock.acquire()/lock.release() লেখেনি; ডেকোরেটর তা নিজে করেছে। কিন্তু BuggyMonitor.produce_two() যখন একই অবজেক্টের self.produce()-কে ভেতর থেকে কল করলো, লক তখনও ধরা ছিল বলে দ্বিতীয় acquire() ব্যর্থ হলো। বাস্তব মনিটরেও এই "রিএন্ট্র্যান্সি" একটি পরিচিত, বাস্তব ডিজাইন-সাবধানতা — কিছু ভাষার লক রিএন্ট্র্যান্ট (একই থ্রেড আবার লক নিতে পারে), কিছুর নয়।
মূল কথা · Key takeaway

মনিটর সেমাফোরকে প্রতিস্থাপন করে না — এটি সেমাফোরের উপরেই তৈরি একটি উচ্চ-স্তরের অ্যাবস্ট্রাকশন যা মিউচুয়াল এক্সক্লুশনের দায়িত্ব প্রোগ্রামার থেকে ভাষা/রানটাইমে সরিয়ে দেয়। ফলাফল: কম বাগ, পরিষ্কার কোড — কিন্তু রিএন্ট্র্যান্সি ও কন্ডিশন ভেরিয়েবলের সঠিক ব্যবহারের মতো নতুন সাবধানতাও নিয়ে আসে।

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

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

প্র ০১ মনিটর সেমাফোরের তুলনায় বাগ কমাতে কীভাবে সাহায্য করে — সঠিকতার দায়িত্বটা আসলে কোথায় সরে যায়?

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

প্র ০২ কন্ডিশন ভেরিয়েবলের wait() সেমাফোরের wait()/P থেকে মূলত কীভাবে আলাদা?

সেমাফোরের wait() শুধু একটি কাউন্টার কমায় এবং প্রয়োজনে ব্লক করে — মনিটরের ধারণা এখানে নেই। কন্ডিশন ভেরিয়েবলের wait() মনিটরের ভেতরে থেকে কল হয় এবং কলিং প্রসেসকে মনিটরের লক সাময়িকভাবে ছেড়ে দিয়ে অপেক্ষায় পাঠায় — যাতে অন্য কেউ মনিটরে ঢুকে সেই শর্ত পূরণ করতে পারে। শর্ত পূরণ হলে signal()/notify() অপেক্ষমাণ প্রসেসকে জাগায়, যে তখন লক পুনরায় নিয়ে চালিয়ে যায়।

প্র ০৩ উপরের কোডে BuggyMonitor.produce_two() কেন RuntimeError দিলো — এটা বাস্তব মনিটর ডিজাইনে কী সমস্যার ইঙ্গিত দেয়?

produce_two() নিজেই @synchronized, তাই কল হওয়ার সাথে সাথেই লক অ্যাকোয়ার হয়ে যায়। এর ভেতর থেকে যখন এটি একই অবজেক্টের self.produce()-কে কল করে, সেই মেথডও লক অ্যাকোয়ার করার চেষ্টা করে — কিন্তু লক তখনও ধরা আছে, তাই আমাদের SimulatedLock ব্যর্থ হয়ে RuntimeError ছোঁড়ে। এটি "রিএন্ট্র্যান্সি" সমস্যার একটি সরল উদাহরণ — বাস্তব মনিটর ডিজাইনে একই থ্রেড থেকে নিজের মধ্যেই আরেকটি synchronized মেথড কল করা নিরাপদ কি না, তা ভাষা/লাইব্রেরি অনুযায়ী স্পষ্টভাবে জানা জরুরি।

অনুশীলন

  1. চিন্তা করুন: এই কোডে consume() বাফার খালি পেলে সাথে সাথে একটি বার্তা রিটার্ন করে ব্যর্থ হয়। একটি বাস্তব মনিটরে (কন্ডিশন ভেরিয়েবলসহ) এর বদলে কী হতো?

    বাস্তব মনিটরে consume() বাফার খালি দেখলে সাথে সাথে ব্যর্থ হয়ে ফিরে আসতো না — বরং একটি কন্ডিশন ভেরিয়েবলে wait() কল করে মনিটরের লক সাময়িকভাবে ছেড়ে দিয়ে ব্লক হয়ে থাকতো। যখন কোনো produce() একটি আইটেম যোগ করতো, সেটি সেই কন্ডিশন ভেরিয়েবলে signal() করে অপেক্ষমাণ consumer-কে জাগাতো, যে তখন লক পুনরায় নিয়ে আইটেমটি নিয়ে যেতে পারতো — L19-এর সেমাফোর-ভিত্তিক ব্লকিংয়ের ঠিক সমতুল্য আচরণ, কিন্তু মনিটরের ভেতর থেকে।

  2. পরীক্ষা করুন: উপরের কোড সেলে MonitorBoundedBuffer(capacity=2)-কে capacity=3 করে Run চেপে দেখুন কতটি produce() কল এখন সফল হয় সেটির পরের consume()-এর আগে।

    capacity=3 হলে produce("A"), produce("B"), produce("C") — তিনটিই সফল হবে (বাফার ['A','B','C']), কারণ তিনটিই ক্যাপাসিটির মধ্যে। প্রথম consume() কল হওয়ার আগেই বাফার পূর্ণ ধারণক্ষমতায় পৌঁছাবে, তাই আর কোনো আইটেম প্রত্যাখ্যাত হবে না যতক্ষণ না চতুর্থ produce() আসে।

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

আগের পাঠ
ক্লাসিক সিনক্রোনাইজেশন: ডাইনিং ফিলোসফার্স