মিউটেক্স লক ও সেমাফোর
এই পাঠে যা শিখবেন
- মিউটেক্স লক ও সেমাফোরের মধ্যে পার্থক্য এবং কেন সেমাফোর বেশি সাধারণ/শক্তিশালী
wait()/P ওsignal()/V অপারেশনের সঠিক সংজ্ঞা — এবং কেন এগুলো অ্যাটমিক হওয়া জরুরি- বাইনারি বনাম কাউন্টিং সেমাফোরের পার্থক্য এবং কাউন্টিং সেমাফোর কীভাবে N-টি রিসোর্সের পুল সামলায়
- Python-এ একটি প্রকৃত
Semaphoreক্লাস (count ও waiting list ট্র্যাক করা) লিখে L17-এর রেস কন্ডিশন ফিক্স করা
১ · মিউটেক্স লক — সবচেয়ে সরল টুল
মিউটেক্স লকMutex (Mutual Exclusion) Lockসবচেয়ে সরল সিনক্রোনাইজেশন প্রিমিটিভ -- একটি বাইনারি লক, acquire()/release() সহ।
হলো L17-এর ক্রিটিক্যাল সেকশন সমস্যার সবচেয়ে সরল সমাধান — একটি বাইনারি লক, যাতে দুটি অপারেশন থাকে:
acquire() (ক্রিটিক্যাল সেকশনে ঢোকার আগে অবশ্যই কল করতে হবে) এবং release()
(বেরিয়ে যাওয়ার পর কল করতে হবে)। যদি লক ইতিমধ্যে অন্য কেউ ধরে রেখেছে, acquire() ব্লক করে থাকে যতক্ষণ
না সেটি রিলিজ হয় (কিছু ইমপ্লিমেন্টেশনে busy-wait করে, যাকে বলে স্পিনলক)। মিউটেক্স যথেষ্ট যখন সমস্যাটা শুধুই
"একসাথে একজন" — কিন্তু বাস্তবে অনেক সিনক্রোনাইজেশন সমস্যা এর চেয়ে জটিল, যেমন "একসাথে সর্বোচ্চ N জন" (L19-এর
বাউন্ডেড বাফার) — সেখানে একটি বেশি সাধারণ টুল দরকার।
২ · সেমাফোর — সাধারণ ইন্টিজার-ভিত্তিক টুল
সেমাফোরSemaphoreএকটি ইন্টিজার-ভ্যালুড সিনক্রোনাইজেশন টুল, দুটি অ্যাটমিক অপারেশন wait()/P এবং signal()/V সহ। হলো একটি ইন্টিজার-ভ্যালুড ভেরিয়েবল, যাতে ঠিক দুটি অ্যাটমিক অপারেশন সংজ্ঞায়িত থাকে:
মান ১ কমায়; যদি মান নেগেটিভ হয়ে যায়, কলকারী প্রসেসকে ব্লক করে দেয় (একটি waiting list-এ যোগ করে)।
মান ১ বাড়ায়; যদি কেউ ব্লকড থাকে, waiting list থেকে একজনকে জাগিয়ে দেয়।
বাইনারি সেমাফোর (মান শুধু ০ বা ১ হতে পারে) সম্পূর্ণভাবে একটি মিউটেক্সের সমতুল্য। কিন্তু কাউন্টিং সেমাফোর (যেকোনো ইন্টিজার মান নিতে পারে) আরও শক্তিশালী — একে N দিয়ে ইনিশিয়ালাইজ করলে, একসাথে সর্বোচ্চ N-টি প্রসেস এগিয়ে যেতে পারে, যা একটি রিসোর্স পুল নিয়ন্ত্রণ করার জন্য একদম উপযুক্ত (এটিই L19-এর producer-consumer সমস্যার সরাসরি প্রিভিউ, যেখানে ঠিক এই ক্ষমতাটাই দরকার হবে)।
৩ · একটি প্রকৃত Semaphore ক্লাস
নিচে একটি সম্পূর্ণ Semaphore ক্লাস লেখা হলো — এটি একটি value এবং একটি waiting
তালিকা ট্র্যাক করে। মনে রাখবেন — এই সাইটের কোড সেলগুলো একক-থ্রেডেড সিমুলেশন (কোনো আসল থ্রেড/প্রসেস চলছে না, OS
CLAUDE.md-এর নিয়ম অনুযায়ী), তাই আমরা wait()/signal()-কে সিকোয়েন্সিয়ালি কল করে এবং
প্রতিবার ফলাফল প্রিন্ট করে তাদের আচরণ ট্রেস করব — এটিই একটি সেমাফোরের অভ্যন্তরীণ hesab (value ও waiting list)
বোঝার সবচেয়ে স্পষ্ট উপায়।
class Semaphore:
def __init__(self, initial_value):
self.value = initial_value
self.waiting = [] # ব্লকড প্রসেসের নামের তালিকা (FIFO)
def wait(self, name): # P operation
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} (ক্রিটিক্যাল সেকশনে প্রবেশ)")
return True # সরাসরি এগিয়ে গেল
def signal(self, name): # V operation
self.value += 1
if self.value <= 0 and self.waiting:
woken = self.waiting.pop(0)
print(f" {name}: signal() -> value={self.value} (জাগ্রত করা হলো: {woken})")
return woken
print(f" {name}: signal() -> value={self.value} (কেউ অপেক্ষমাণ নেই)")
return None
print("== Demo ১: বাইনারি সেমাফোর -- value ও waiting list ট্র্যাক করা ==")
demo = Semaphore(1)
demo.wait("A") # value 1 -> 0, সরাসরি প্রবেশ
demo.wait("B") # value 0 -> -1, ব্লকড
demo.signal("A") # value -1 -> 0, B জাগে
print()
print("== Demo ২: L17-এর shared_counter, এবার mutex দিয়ে প্রোটেক্টেড ==")
mutex = Semaphore(1)
shared_counter = 0
def protected_increment(name):
global shared_counter
mutex.wait(name)
# ক্রিটিক্যাল সেকশন -- strictly sequential acquire-modify-release, তাই কোনো ইন্টারলিভিং সম্ভব নয়
current = shared_counter
current += 1
shared_counter = current
print(f" {name}: increment সম্পন্ন -> shared_counter={shared_counter}")
mutex.signal(name)
for name in ["P1", "P2", "P1", "P2"]: # আগের মতোই মোট ৪টি increment-এর চেষ্টা
protected_increment(name)
expected = 4
print()
print("ফাইনাল shared_counter =", shared_counter)
print("প্রত্যাশিত =", expected)
print("মিলেছে -- এবার আর কোনো লস্ট আপডেট নেই!" if shared_counter == expected else "মিলেনি!")
mutex.wait(name) প্রতিটি increment-এর read-add-write তিন ধাপকে
একটি অবিভাজ্য ব্লকে পরিণত করেছে। L17-এর মতো আর কোনো প্রসেস মাঝপথে ঢুকে stale মান পড়তে পারছে না, কারণ পরের প্রসেস
ততক্ষণ পর্যন্ত mutex.wait()-এই আটকে থাকবে (আমাদের সিমুলেশনে: ব্লকড হয়ে যাবে) যতক্ষণ না আগেরজন
mutex.signal() কল করে। ফলাফলে ফাইনাল কাউন্টার ঠিক প্রত্যাশিত ৪-এ পৌঁছায়।
মিউটেক্স ও বাইনারি সেমাফোর কার্যত একই জিনিস — "একসাথে একজন" নিয়ন্ত্রণ করে। কিন্তু সেমাফোরের আসল শক্তি কাউন্টিং
ভ্যারিয়েন্টে — যেখানে initial value N দিয়ে একসাথে N-টি প্রসেসকে এগিয়ে যেতে দেওয়া যায়। এই একই
Semaphore ক্লাস (অপরিবর্তিত wait()/signal() লজিকসহ) এখন থেকে L19-এর
producer-consumer (তিনটি সেমাফোর: empty, full, mutex) এবং L20-এর readers-writers (mutex ও resource_lock)
সমস্যা সমাধানে সরাসরি পুনরায় ব্যবহার করা হবে — একই আচরণ, ভিন্ন প্রয়োগ।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ সেমাফোরের value কেন নেগেটিভ হতে পারে? এই নেগেটিভ মানটি ঠিক কী বোঝায়?
একটি সেমাফোরের value যখন নেগেটিভ হয়, এর পরম মান (absolute value) ঠিক ততজন প্রসেস এই মুহূর্তে ব্লকড হয়ে
waiting list-এ অপেক্ষা করছে তা বোঝায়। যেমন value = -2 মানে দুইজন প্রসেস signal() কল হওয়ার
অপেক্ষায় আছে। এটি সেমাফোরকে শুধু "কতগুলো রিসোর্স ফাঁকা আছে" তা নয়, বরং "কতজন অপেক্ষা করছে" তাও একই ভেরিয়েবলে
এনকোড করতে দেয় — উপরের কোড সেলের Demo ১-এ এটি সরাসরি দেখা গেছে।
প্র ০২ কেন wait() ও signal() অপারেশন দুটোকে "অ্যাটমিক" হতে হবে? যদি এগুলো নিজেরাই একাধিক সাব-স্টেপে ভাঙা যেত, তাহলে কী সমস্যা হতো?
যদি wait()-এর ভেতরের "value কমানো, তারপর চেক করা" ধাপগুলো নিজেরাই ইন্টারাপ্ট হতে পারত, তাহলে
দুটি প্রসেস একইসাথে wait() কল করলে ঠিক L17-এর মতোই একটি নতুন রেস কন্ডিশন তৈরি হতো — যে সমস্যাটা
সমাধান করতেই সেমাফোর তৈরি হয়েছে! তাই বাস্তব OS-এ wait()/signal() হার্ডওয়্যার-সমর্থিত
অ্যাটমিক instruction (যেমন test-and-set) দিয়ে বাস্তবায়িত হয় — এই পাঠের সিমুলেশনে আমরা এটি ধরেই নিয়েছি
(প্রতিটি কল একবারে, বিভক্ত না হয়ে সম্পন্ন হয়)।
প্র ০৩ Demo ২-তে যদি ভুলবশত mutex.signal() কল করতে ভুলে যাওয়া হতো (শুধু mutex.wait() থাকত), তাহলে কী হতো?
প্রথম প্রসেস mutex.wait() কল করে ক্রিটিক্যাল সেকশনে ঢুকত (value 1 থেকে 0), কিন্তু কখনো signal() কল না করলে value আর কখনো বাড়ত না। ফলে দ্বিতীয় প্রসেস mutex.wait() কল করলে value 0 থেকে -1 হয়ে চিরকালের জন্য ব্লকড হয়ে যেত — কেউ কখনো তাকে জাগাতে পারত না, কারণ কেউ আর কখনো signal() কল করছে না। এটি একটি "লিকড লক" (leaked lock) — বাস্তব সিস্টেমে সবচেয়ে সাধারণ সিনক্রোনাইজেশন বাগগুলোর একটি, এবং M6-এর ডেডলক আলোচনার সাথে সরাসরি সম্পর্কিত।
অনুশীলন
-
পরীক্ষা করুন: উপরের কোড সেলে
demo = Semaphore(1)-কেdemo = Semaphore(2)করে Run চাপুন। এখন কি "A" ও "B" উভয়েই ব্লক না হয়ে সরাসরি এগিয়ে যেতে পারে?হ্যাঁ।
Semaphore(2)-এ প্রথমwait("A")value কে 2 থেকে 1 করবে (এখনো ≥ 0, তাই প্রবেশ), আর দ্বিতীয়wait("B")value কে 1 থেকে 0 করবে (তখনও ≥ 0, তাই প্রবেশ) — কেউই ব্লক হবে না। এটিই কাউন্টিং সেমাফোরের মূল ক্ষমতা — একসাথে একাধিক (এখানে দুইজন পর্যন্ত) প্রসেসকে এগিয়ে যেতে দেওয়া। -
চিন্তা করুন: Demo ২-এর
schedule-এ ৪-এর জায়গায় ৬টি increment (৩টি করে P1, P2) চেষ্টা করলে ফাইনাল কাউন্টার কত হওয়া উচিত? কেন?ফাইনাল কাউন্টার হবে ৬ — মিউটেক্স-প্রোটেক্টেড increment-এ প্রতিটি অনুরোধ সঠিকভাবে, একে একে, কোনো ওভারল্যাপ ছাড়াই সম্পন্ন হয়, তাই যতগুলো
protected_increment()কল হবে, শেয়ার্ড কাউন্টার ঠিক ততবারই নির্ভুলভাবে বাড়বে — L17-এর কোঅর্ডিনেশন-বিহীন সংস্করণের মতো কোনো লস্ট আপডেট আর হবে না।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরের পাঠ: ক্লাসিক সিনক্রোনাইজেশন — প্রডিউসার-কনজিউমার L19 এই পাঠের Semaphore ক্লাস দিয়েই তিনটি সেমাফোর (empty, full, mutex) ব্যবহার করে বাউন্ডেড বাফার সমস্যা সমাধান।
- পূর্বের পাঠ ফিরে দেখুন: ক্রিটিক্যাল সেকশন প্রবলেম L17 লস্ট-আপডেট সমস্যার আসল রূপ, যা এই পাঠে ফিক্স করা হলো।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ M5-এর বাকি পাঠগুলো — producer-consumer, readers-writers, dining philosophers ও মনিটর।