মনিটর ও উচ্চ-স্তরের সিনক্রোনাইজেশন কনস্ট্রাক্ট
এই পাঠে যা শিখবেন
- সেমাফোর-ভিত্তিক সমাধান কেন বাস্তব বড় কোডবেসে বাগ-প্রবণ, তার সুনির্দিষ্ট কারণ
- মনিটর কী, এটি কীভাবে মিউটেক্স স্বয়ংক্রিয়ভাবে বলবৎ করে, এবং কন্ডিশন ভেরিয়েবল কীভাবে কাজ করে
- বাস্তব ভাষায় মনিটরের প্রয়োগ — Java
synchronized, Pythonthreading.Condition - Python-এ একটি সরল "মনিটর-স্টাইল" bounded buffer ক্লাস বানিয়ে L19-এর ম্যানুয়াল সেমাফোর সমাধানের সাথে সরাসরি তুলনা
১ · সেমাফোরের আসল সমস্যাটা কী
L18-L21-এ আমরা সেমাফোর দিয়ে ক্রিটিক্যাল সেকশন, প্রডিউসার-কনজিউমার, রিডার্স-রাইটার্স ও ডাইনিং ফিলোসফার্স সমাধান
করেছি। প্রতিটি ক্ষেত্রেই সমাধান কাজ করেছে — কিন্তু একটি লুকানো শর্তে: প্রোগ্রামারকে প্রতিটি শেয়ার্ড ডেটা
অ্যাক্সেসের আগে-পরে সঠিক ক্রমে ঠিক wait()/signal() কল বসাতে হয়েছে। একটি বড় কোডবেসে,
যেখানে শত শত জায়গায় শেয়ার্ড ডেটা অ্যাক্সেস হয়, একটিমাত্র ভুলে-বসানো বা ভুলে-যাওয়া কল —
- একটি
signal()ভুলে বাদ পড়লে অন্য সব প্রসেস চিরকালের জন্য আটকে যেতে পারে, - দুটি
wait()ভুল ক্রমে বসালে ডেডলক হতে পারে (M6-এ বিস্তারিত), - কোনো একটি অ্যাক্সেস পয়েন্টে
wait()/signal()বসাতেই ভুলে গেলে চুপচাপ একটি রেস কন্ডিশন থেকে যায় — কম্পাইলার বা রানটাইম কোনো সতর্কতা দেয় না।
২ · মনিটর কী
মনিটর (Monitor)Monitorএকটি উচ্চ-স্তরের সিনক্রোনাইজেশন কনস্ট্রাক্ট যা শেয়ার্ড ডেটা ও তার উপর কাজ করা প্রসিডিওরগুলোকে একসাথে বান্ডেল করে, এবং নিশ্চিত করে একসময়ে কেবল একটি প্রসেস/থ্রেড মনিটরের ভেতরে সক্রিয় থাকতে পারবে। হলো একটি প্রোগ্রামিং-ভাষা-সমর্থিত কনস্ট্রাক্ট যা শেয়ার্ড ডেটা এবং সেই ডেটার উপর কাজ করা প্রসিডিওরগুলোকে একসাথে একটি একক ইউনিটে আবদ্ধ করে। মূল গ্যারান্টি: একসময়ে কেবলমাত্র একটি প্রসেস/থ্রেড মনিটরের ভেতরে সক্রিয়ভাবে চলতে পারবে — এই মিউচুয়াল এক্সক্লুশন প্রোগ্রামার নিজে বসায় না, ভাষা বা রানটাইম নিজেই স্বয়ংক্রিয়ভাবে বলবৎ করে। ফলে সেমাফোরের মতো "প্রতিটি জায়গায় সঠিক ক্রমে wait/signal বসিয়েছি তো?" প্রশ্নটাই আর থাকে না।
৩ · কন্ডিশন ভেরিয়েবল
মনিটরের ভেতরে ঢুকেই যদি কোনো প্রসেস দেখে তার দরকারি শর্ত এখনো সত্যি নয় (যেমন বাফার খালি, তাই consume করার কিছু
নেই), তাকে কিছু একটা করতে হবে — কিন্তু মনিটরের ভেতরে বসেই অপেক্ষা করলে অন্য কেউ (যে ঐ শর্ত পূরণ করতে পারত) মনিটরে
ঢুকতেই পারবে না, ফলে ডেডলক। এই সমস্যার সমাধান
কন্ডিশন ভেরিয়েবল (Condition Variable)Condition Variableমনিটরের ভেতরে থাকা একটি প্রসেসকে মনিটরের লক সাময়িকভাবে ছেড়ে দিয়ে অপেক্ষায় পাঠানোর প্রক্রিয়া (wait), এবং শর্ত পূরণ হলে কাউকে জাগানোর প্রক্রিয়া (signal) — সেমাফোরের wait()/signal() থেকে ভিন্ন একটি মেকানিজম।
— এটি একটি প্রসেসকে মনিটরের লক সাময়িকভাবে ছেড়ে দিয়ে অপেক্ষায় পাঠাতে দেয় (wait()), যাতে
অন্যরা ভেতরে ঢুকে শর্তটি পূরণ করতে পারে; শর্ত পূরণ হলে অপেক্ষমাণ কাউকে জাগানো হয় (signal()/
notify()), যে তখন লক পুনরায় নিয়ে চালিয়ে যায়। এটি সেমাফোরের wait()/signal()
থেকে ভিন্ন — সেমাফোরের wait() মনিটরের কোনো লক ছাড়ে না, কারণ সেমাফোরে "মনিটর" ধারণাটাই নেই।
synchronized + wait()/notify()একটি মেথড বা ব্লককে
synchronized চিহ্নিত করলে JVM নিজেই সেই অবজেক্টের লক অ্যাকোয়ার/রিলিজ করে; ভেতরে wait()/notify() কন্ডিশন ভেরিয়েবলের কাজ করে।threading.Conditionএকটি অন্তর্নিহিত লকসহ কন্ডিশন ভেরিয়েবল দেয় —
with cond: ব্লকে ঢুকে cond.wait()/cond.notify() কল করা যায়, লক ম্যানেজমেন্ট স্বয়ংক্রিয়।৪ · একটি মনিটর-স্টাইল bounded buffer
নিচে L19-এর 3-সেমাফোর প্রডিউসার-কনজিউমার সমাধানের বিপরীতে একটি "মনিটর-স্টাইল" bounded buffer দেখানো হলো —
এখানেও বাস্তব থ্রেড নয়, শুধুই একটি ইন-মেমরি লক-সিমুলেশন (SimulatedLock) যা দেখায় মনিটরের মূল
ধারণা: caller নিজে কখনো লক অ্যাকোয়ার/রিলিজ করছে না — একটি ডেকোরেটর প্রতিটি পাবলিক মেথডের শুরুতে-শেষে
স্বয়ংক্রিয়ভাবে তা করছে।
# সিমুলেটেড মনিটর -- বাস্তব থ্রেড/লক নয়, শুধুই একটি ইন-মেমরি লক-সিমুলেশন ধারণা বোঝানোর জন্য
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() ব্যর্থ হলো। বাস্তব মনিটরেও এই "রিএন্ট্র্যান্সি" একটি
পরিচিত, বাস্তব ডিজাইন-সাবধানতা — কিছু ভাষার লক রিএন্ট্র্যান্ট (একই থ্রেড আবার লক নিতে পারে), কিছুর নয়।
মনিটর সেমাফোরকে প্রতিস্থাপন করে না — এটি সেমাফোরের উপরেই তৈরি একটি উচ্চ-স্তরের অ্যাবস্ট্রাকশন যা মিউচুয়াল এক্সক্লুশনের দায়িত্ব প্রোগ্রামার থেকে ভাষা/রানটাইমে সরিয়ে দেয়। ফলাফল: কম বাগ, পরিষ্কার কোড — কিন্তু রিএন্ট্র্যান্সি ও কন্ডিশন ভেরিয়েবলের সঠিক ব্যবহারের মতো নতুন সাবধানতাও নিয়ে আসে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ মনিটর সেমাফোরের তুলনায় বাগ কমাতে কীভাবে সাহায্য করে — সঠিকতার দায়িত্বটা আসলে কোথায় সরে যায়?
সেমাফোরে, প্রতিটি শেয়ার্ড-ডেটা অ্যাক্সেসের আগে-পরে সঠিক ক্রমে wait()/signal()
বসানো সম্পূর্ণভাবে প্রোগ্রামারের দায়িত্ব — কোডবেস জুড়ে ছড়িয়ে থাকা শত শত জায়গায় এটি ভুল হওয়ার সুযোগ থাকে।
মনিটরে, শেয়ার্ড ডেটা ও তার প্রসিডিওর একটি ক্লাস/মডিউলে বন্দি থাকে, আর মিউচুয়াল এক্সক্লুশন ভাষা/রানটাইম
নিজেই স্বয়ংক্রিয়ভাবে প্রয়োগ করে — প্রোগ্রামারকে শুধু মনিটরের ভেতরে সঠিক লজিক লিখতে হয়, লক ম্যানেজমেন্ট
নয়।
প্র ০২
কন্ডিশন ভেরিয়েবলের wait() সেমাফোরের wait()/P থেকে মূলত কীভাবে আলাদা?
সেমাফোরের wait() শুধু একটি কাউন্টার কমায় এবং প্রয়োজনে ব্লক করে — মনিটরের ধারণা এখানে নেই।
কন্ডিশন ভেরিয়েবলের wait() মনিটরের ভেতরে থেকে কল হয় এবং কলিং প্রসেসকে মনিটরের লক
সাময়িকভাবে ছেড়ে দিয়ে অপেক্ষায় পাঠায় — যাতে অন্য কেউ মনিটরে ঢুকে সেই শর্ত পূরণ করতে পারে। শর্ত পূরণ হলে
signal()/notify() অপেক্ষমাণ প্রসেসকে জাগায়, যে তখন লক পুনরায় নিয়ে চালিয়ে যায়।
প্র ০৩
উপরের কোডে BuggyMonitor.produce_two() কেন RuntimeError দিলো — এটা বাস্তব মনিটর ডিজাইনে কী সমস্যার ইঙ্গিত দেয়?
produce_two() নিজেই @synchronized, তাই কল হওয়ার সাথে সাথেই লক অ্যাকোয়ার হয়ে
যায়। এর ভেতর থেকে যখন এটি একই অবজেক্টের self.produce()-কে কল করে, সেই মেথডও লক অ্যাকোয়ার
করার চেষ্টা করে — কিন্তু লক তখনও ধরা আছে, তাই আমাদের SimulatedLock ব্যর্থ হয়ে
RuntimeError ছোঁড়ে। এটি "রিএন্ট্র্যান্সি" সমস্যার একটি সরল উদাহরণ — বাস্তব মনিটর
ডিজাইনে একই থ্রেড থেকে নিজের মধ্যেই আরেকটি synchronized মেথড কল করা নিরাপদ কি না, তা ভাষা/লাইব্রেরি
অনুযায়ী স্পষ্টভাবে জানা জরুরি।
অনুশীলন
-
চিন্তা করুন: এই কোডে
consume()বাফার খালি পেলে সাথে সাথে একটি বার্তা রিটার্ন করে ব্যর্থ হয়। একটি বাস্তব মনিটরে (কন্ডিশন ভেরিয়েবলসহ) এর বদলে কী হতো?বাস্তব মনিটরে
consume()বাফার খালি দেখলে সাথে সাথে ব্যর্থ হয়ে ফিরে আসতো না — বরং একটি কন্ডিশন ভেরিয়েবলেwait()কল করে মনিটরের লক সাময়িকভাবে ছেড়ে দিয়ে ব্লক হয়ে থাকতো। যখন কোনোproduce()একটি আইটেম যোগ করতো, সেটি সেই কন্ডিশন ভেরিয়েবলেsignal()করে অপেক্ষমাণ consumer-কে জাগাতো, যে তখন লক পুনরায় নিয়ে আইটেমটি নিয়ে যেতে পারতো — L19-এর সেমাফোর-ভিত্তিক ব্লকিংয়ের ঠিক সমতুল্য আচরণ, কিন্তু মনিটরের ভেতর থেকে। -
পরীক্ষা করুন: উপরের কোড সেলে
MonitorBoundedBuffer(capacity=2)-কেcapacity=3করে Run চেপে দেখুন কতটিproduce()কল এখন সফল হয় সেটির পরেরconsume()-এর আগে।capacity=3 হলে
produce("A"),produce("B"),produce("C")— তিনটিই সফল হবে (বাফার['A','B','C']), কারণ তিনটিই ক্যাপাসিটির মধ্যে। প্রথমconsume()কল হওয়ার আগেই বাফার পূর্ণ ধারণক্ষমতায় পৌঁছাবে, তাই আর কোনো আইটেম প্রত্যাখ্যাত হবে না যতক্ষণ না চতুর্থproduce()আসে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- মিউটেক্স লক ও সেমাফোর L18 এই পাঠের মনিটর যে নিম্ন-স্তরের প্রিমিটিভের উপর তৈরি, তার ভিত্তি আরেকবার দেখে নিন।
- ডেডলক — চারটি প্রয়োজনীয় শর্ত পরবর্তী পাঠ · M6 সিনক্রোনাইজেশন প্রিমিটিভ ভুলভাবে ব্যবহার হলে যে সবচেয়ে গুরুতর সমস্যা হতে পারে — ডেডলক — তার মডিউল এখন শুরু হচ্ছে।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ প্রসেস সিনক্রোনাইজেশন মডিউল (M5) এখানেই শেষ — পরবর্তী মডিউল ডেডলক নিয়ে।