ক্লাসিক সিনক্রোনাইজেশন: রিডার্স-রাইটার্স
এই পাঠে যা শিখবেন
- রিডার্স-রাইটার্স সমস্যার সংজ্ঞা এবং কেন এটি প্রডিউসার-কনজিউমার থেকে ভিন্ন ধরনের চ্যালেঞ্জ
- ক্লাসিক (first readers-writers) সমাধান — reader_count, mutex, resource_lock-এর ভূমিকা
- কেন প্রথম ও শেষ রিডারই একমাত্র resource_lock স্পর্শ করে, মাঝের রিডাররা নয়
- রাইটার-স্টারভেশনের বাস্তব ঝুঁকি এবং ফেয়ারার ভ্যারিয়েন্টের সংক্ষিপ্ত পরিচিতি
১ · রিডার্স-রাইটার্স সমস্যা
রিডার্স-রাইটার্স সমস্যাReaders-Writers Problemশেয়ার্ড ডেটা একাধিক রিডার একসাথে পড়তে পারে, কিন্তু একজন রাইটারের দরকার সম্পূর্ণ এক্সক্লুসিভ অ্যাক্সেস। -য় একটি মৌলিক পর্যবেক্ষণ কাজে লাগে — একাধিক রিডার শেয়ার্ড ডেটা একসাথে পড়লে কোনো সমস্যা নেই (কেউ কিছু পরিবর্তন করছে না, তাই কনফ্লিক্ট নেই), কিন্তু একজন রাইটার লিখতে থাকলে অন্য কোনো রিডার বা রাইটার একই সময়ে সেই ডেটা স্পর্শ করতেই পারবে না। এটি L19-এর প্রডিউসার-কনজিউমার থেকে ভিন্ন — সেখানে সবাইকে সিরিয়ালাইজ করতে হতো, কিন্তু এখানে "একাধিক রিডার একসাথে" আসলে একটি বৈধ, কাম্য অবস্থা — নতুন করে চিন্তা করার দরকার হয় ঠিক এই বিশেষ সুবিধাটুকু কীভাবে কাজে লাগানো যায়, অথচ রাইটারের এক্সক্লুসিভিটিও নিশ্চিত থাকে।
২ · ক্লাসিক সমাধান — reader_count, mutex, resource_lock
এই মুহূর্তে কতজন রিডার সক্রিয়ভাবে পড়ছে তার কাউন্টার।
বাইনারি সেমাফোর — শুধু
reader_count-কে নিরাপদে বাড়ানো/কমানোর জন্য।বাইনারি সেমাফোর — প্রকৃত শেয়ার্ড রিসোর্সের এক্সক্লুসিভ অ্যাক্সেস নিয়ন্ত্রণ করে (রাইটার সরাসরি এটি ধরে; রিডাররা পরোক্ষভাবে, শুধু প্রথম/শেষজন)।
মূল কৌশলটি হলো — প্রথম রিডার যখন আসে, সে resource_lock নিজে অ্যাকোয়ার করে (এতে
কোনো রাইটার আর ঢুকতে পারবে না)। এরপর যত রিডারই আসুক, তারা শুধু reader_count বাড়িয়ে সরাসরি পড়া শুরু
করে দেয় — resource_lock-এর জন্য আর অপেক্ষা করতে হয় না, কারণ সেটা ইতিমধ্যে "রিডারদের পক্ষ থেকে" ধরাই
আছে। যখন শেষ রিডার (reader_count আবার ০-তে ফিরে গেলে) চলে যায়, সে-ই resource_lock
রিলিজ করে — তখনই কোনো অপেক্ষমাণ রাইটার সুযোগ পায়।
৩ · কোডে ক্লাসিক সমাধান
নিচের কোডে start_read()/end_read() ফাংশন reader_count
বাড়ায়/কমায় এবং শুধু প্রথম/শেষ রিডারের ক্ষেত্রেই resource_lock অ্যাকোয়ার/রিলিজ করে, আর
write() সরাসরি resource_lock অ্যাকোয়ার/রিলিজ করে। L18-এর Semaphore
ক্লাসই এখানে (অপরিবর্তিতভাবে) ব্যবহৃত হচ্ছে।
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
reader_count = 0
mutex = Semaphore(1) # reader_count প্রোটেক্ট করে
resource_lock = Semaphore(1) # প্রকৃত শেয়ার্ড রিসোর্স
shared_data = "v1"
pending_writer = [None]
def start_read(name):
global reader_count
print(f"\n{name}: start_read()")
mutex.wait(name)
reader_count += 1
print(f" reader_count = {reader_count}")
if reader_count == 1:
print(" প্রথম রিডার -> resource_lock acquire")
resource_lock.wait(name)
mutex.signal(name)
def end_read(name):
global reader_count
print(f"\n{name}: end_read()")
mutex.wait(name)
reader_count -= 1
print(f" reader_count = {reader_count}")
if reader_count == 0:
print(" শেষ রিডার -> resource_lock release")
woken = resource_lock.signal(name)
if woken:
resume_writer_after_wakeup()
else:
mutex.signal(name)
return
mutex.signal(name)
def write(name, new_value):
global shared_data
print(f"\n{name}: write('{new_value}') চেষ্টা")
ok = resource_lock.wait(name)
if not ok:
pending_writer[0] = (name, new_value)
print(f" {name} blocked -- resource ফ্রি না হওয়া পর্যন্ত অপেক্ষা")
return
shared_data = new_value
print(f" shared_data আপডেট হলো -> {shared_data}")
resource_lock.signal(name)
def resume_writer_after_wakeup():
global shared_data
name, new_value = pending_writer[0]
print(f" [ব্লকড writer {name} এখন জাগ্রত]")
shared_data = new_value
print(f" shared_data আপডেট হলো -> {shared_data}")
resource_lock.signal(name)
start_read("Reader1")
start_read("Reader2") # Reader1-এর সাথে ওভারল্যাপ -- অতিরিক্ত ব্লকিং নেই
print(f"\nএখন একসাথে active reader_count = {reader_count} (দুইজন রিডার একসাথে পড়ছে)")
write("Writer1", "v2") # resource_lock রিডারদের দখলে -- BLOCKED হবে
end_read("Reader1") # reader_count=1, এখনো Reader2 active, resource_lock ছাড়বে না
end_read("Reader2") # last reader -- resource_lock release, Writer1 জাগ্রত হবে
print("\nফাইনাল shared_data:", shared_data)
print("ফাইনাল reader_count:", reader_count)
print("ফাইনাল resource_lock.value:", resource_lock.value)
Reader2-এর start_read() কল resource_lock মোটেও স্পর্শ
করে না (কারণ reader_count 1 থেকে 2 হয়, ১ নয়), তাই সে কোনো ব্লকিং ছাড়াই সরাসরি এগিয়ে যায় — এটিই দুইজন রিডারের
"একসাথে সক্রিয়" থাকা। অন্যদিকে Writer1 resource_lock.wait()-এ ব্লক হয়ে যায়, কারণ
সেটি ইতিমধ্যে Reader1 দ্বারা অ্যাকোয়ার করা আছে — এবং শুধুমাত্র শেষ রিডার (Reader2) বিদায় নেওয়ার পরই সে জাগে।
৪ · রাইটার-স্টারভেশন — একটি গুরুত্বপূর্ণ ট্রেড-অফ
এই ক্লাসিক সমাধানের একটি বাস্তব সমস্যা আছে — যদি রিডার ক্রমাগত আসতেই থাকে (একজন শেষ হওয়ার আগেই আরেকজন শুরু হয়ে
যায়), reader_count কখনো ০-তে ফিরে না-ও যেতে পারে, ফলে resource_lock কখনো রিলিজ হয়
না এবং অপেক্ষমাণ রাইটার চিরকাল স্টারভStarvationএকটি প্রসেস বারবার সুযোগ থেকে বঞ্চিত হয়ে অনির্দিষ্টকাল অপেক্ষা করতে থাকা।
করতে পারে। এই সমাধানকে তাই বলা হয় "first readers-writers" — রিডারদের অগ্রাধিকার দেয়। "সেকেন্ড readers-writers"
এবং আরও ফেয়ার, কিউ-ভিত্তিক ভ্যারিয়েন্টগুলো এই সমস্যা সমাধান করে — একজন রাইটার অপেক্ষা করা শুরু করলে নতুন আসা
রিডারদের তার পরে রাখা হয় — কিন্তু এর বিনিময়ে রিডারদের থ্রুপুট কিছুটা কমে যায় (এটিই একটি সত্যিকারের, সুপরিচিত
ট্রেড-অফ, L17-এর bounded waiting শর্তের সরাসরি বাস্তব উদাহরণ)।
রিডার্স-রাইটার্স সমস্যা দেখায় যে সব সিনক্রোনাইজেশন সমস্যার সমাধান "সম্পূর্ণ সিরিয়ালাইজেশন" নয় — কখনো কখনো একাধিক অ্যাক্সেসকারী নিরাপদে একসাথে চলতে পারে, আর সমাধানের কাজ হলো ঠিক কখন সেই সমান্তরাল অ্যাক্সেস নিরাপদ আর কখন নয় তা নির্ভুলভাবে ট্র্যাক করা। পরের পাঠে (L21) আমরা দেখব ডাইনিং ফিলোসফার্স সমস্যা — যেখানে ভুল অর্ডারিং সরাসরি একটি সম্পূর্ণ ডেডলকের দিকে নিয়ে যায়, যা M6-এর ডেডলক মডিউলের প্রাকৃতিক সূচনা।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ কেন মাঝের রিডাররা (প্রথম বা শেষ নয়) কখনো resource_lock স্পর্শ করে না? এটি কি কোনো নিরাপত্তার ঝুঁকি তৈরি করে?
না, কারণ প্রথম রিডার ইতিমধ্যে resource_lock অ্যাকোয়ার করে নিয়েছে বলেই রাইটার আর ঢুকতে পারবে
না — মাঝের রিডারদের জন্য সেই সুরক্ষা ইতিমধ্যেই বলবৎ আছে। মাঝের রিডাররা যদি আবার নিজে resource_lock
অ্যাকোয়ার করার চেষ্টা করত, তারা নিজেদের একজন রিডারের কাছেই ব্লক হয়ে যেত (যেহেতু এটি একটি বাইনারি লক) —
অথচ দুই রিডারের একসাথে পড়ায় কোনো সমস্যা নেই। তাই শুধু reader_count-এর মাধ্যমে গণনা রাখাই যথেষ্ট, এবং এটিই
দক্ষতার মূল কারণ।
প্র ০২ reader_count নিজেই কেন mutex দিয়ে প্রোটেক্ট করা দরকার — এটা কি নিজেই শেয়ার্ড ডেটা নয়?
হ্যাঁ, reader_count নিজেই একটি শেয়ার্ড ভেরিয়েবল — একাধিক রিডার একসাথে start_read()
কল করলে তারা একসাথে এটি বাড়ানোর চেষ্টা করতে পারে, যা ঠিক L17-এর মতোই একটি লস্ট-আপডেট রেস কন্ডিশন ঘটাতে পারে
(যেমন দুইজন একসাথে reader_count পড়ে, দুজনেই ১ যোগ করে, কিন্তু দুজনেরই write একে অপরকে ওভাররাইট করে ফেলে)।
তাই reader_count-এর read-modify-write নিজেই একটি ছোট ক্রিটিক্যাল সেকশন, আর mutex ঠিক সেটাকেই প্রোটেক্ট করছে।
প্র ০৩ উপরের কোড সেলে যদি Reader2 আসার আগেই Writer1 নিজের write() কল করত (Reader1 এখনো পড়ছে, কিন্তু Reader2 আসেনি), তাহলে কী হতো?
Writer1 তখনও resource_lock.wait()-এ ব্লক হতো, কারণ Reader1 ইতিমধ্যে সেটি অ্যাকোয়ার করে রেখেছে
(reader_count=1 হওয়ার সময়ই)। Reader2 আসুক বা না আসুক, resource_lock তখনও রিডারদের দখলে থাকবে যতক্ষণ না
reader_count আবার ০-তে ফেরে (অর্থাৎ সব সক্রিয় রিডার end_read() কল করে)। তাই এই নির্দিষ্ট সিকোয়েন্সে ফলাফল
একই থাকত — শুধু Reader1 একাই থাকলে সেই একজনই "শেষ রিডার" হয়ে resource_lock রিলিজ করত।
অনুশীলন
-
পরীক্ষা করুন: উপরের কোড সেলে
write("Writer1", "v2")-এর আগে একটি তৃতীয়start_read("Reader3")যোগ করে Run চাপুন — reader_count কী দেখায়, এবং Writer1 কি তাও ব্লক থাকে?reader_count হবে ৩ (Reader1, Reader2, Reader3 তিনজনই সক্রিয়), এবং Writer1 তখনও ব্লক থাকবে, কারণ resource_lock এখনও রিডারদের দখলে (যতক্ষণ reader_count > 0)। এটি দেখায় — যতজন রিডারই থাকুক না কেন, রাইটারের জন্য শর্ত একই — শূন্যে ফেরা পর্যন্ত অপেক্ষা।
-
চিন্তা করুন: "সেকেন্ড readers-writers" সমাধানে একজন রাইটার অপেক্ষা শুরু করলে নতুন আসা রিডারদের তার পরে সারিবদ্ধ করা হয়। এটি কীভাবে বাস্তবায়ন করা যেতে পারে বলে মনে হয়?
একটি সম্ভাব্য উপায় — একটি অতিরিক্ত সেমাফোর বা ফ্ল্যাগ যোগ করা যা বোঝায় "একজন রাইটার অপেক্ষা করছে"; নতুন রিডাররা
start_read()-এ ঢোকার আগে প্রথমে এই ফ্ল্যাগ চেক করবে — যদি সত্যি হয়, তারাও অপেক্ষা করবে (এমনকি resource_lock ফাঁকা থাকলেও), যাতে অপেক্ষমাণ রাইটার নতুন রিডারদের পেছনে না পড়ে যায়। এটি ঠিক L17-এর bounded waiting শর্তকে বাস্তবায়ন করার একটি ধরন — রাইটারের জন্য একটি upper bound নিশ্চিত করা।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরের পাঠ: ক্লাসিক সিনক্রোনাইজেশন — ডাইনিং ফিলোসফার্স L21 Dijkstra-র ক্লাসিক ডেডলক দৃষ্টান্ত — M6-এর ডেডলক মডিউলের সরাসরি সেতু।
- পূর্বের পাঠ ফিরে দেখুন: প্রডিউসার-কনজিউমার L19 তিন-সেমাফোর প্যাটার্নের প্রথম প্রয়োগ, যার mutex এখানেও পুনরায় ব্যবহৃত হলো।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ M5-এর শেষ পাঠ — dining philosophers ও মনিটর।