ক্রিটিক্যাল সেকশন প্রবলেম
এই পাঠে যা শিখবেন
- রেস কন্ডিশন ঠিক কী এবং কেন এটি শেয়ার্ড-মেমরি কোঅর্ডিনেশনের একটি স্বাভাবিক পরিণতি
- ক্রিটিক্যাল সেকশনের সংজ্ঞা ও একটি প্রসেসের কোডের সাধারণ কাঠামো (entry, critical, exit, remainder section)
- একটি সঠিক ক্রিটিক্যাল-সেকশন সমাধানের তিনটি অবশ্যপালনীয় শর্ত — এবং প্রতিটি ভঙ্গ হলে কী সমস্যা হয়
- Python দিয়ে একটি প্রকৃত রেস কন্ডিশন সিমুলেট করা — ডেলিবারেটলি ইন্টারলিভড সাব-স্টেপের মাধ্যমে একটি increment হারিয়ে যাওয়া দেখা
১ · রেস কন্ডিশন কী
L08-এ আমরা দেখেছিলাম শেয়ার্ড মেমরি IPCShared Memory IPCএকাধিক প্রসেস একটি সাধারণ মেমরি অঞ্চল সরাসরি পড়তে/লিখতে পারে — দ্রুত, কিন্তু কোঅর্ডিনেশনের দায়িত্ব প্রসেসগুলোর নিজেদের। — যেখানে একাধিক প্রসেস সরাসরি একটি শেয়ার্ড মেমরি অঞ্চল অ্যাক্সেস করে। কিন্তু এই সুবিধারই একটি বিপজ্জনক দিক আছে — যদি দুই বা ততোধিক প্রসেস/থ্রেড কোনো কোঅর্ডিনেশন ছাড়াই একই শেয়ার্ড ডেটা একসাথে পড়ে/লেখে, তাহলে চূড়ান্ত ফলাফল নির্ভর করে ফেলে তাদের এক্সিকিউশনের অনির্দেশ্য টাইমিং বা ইন্টারলিভিং-এর ওপর — একে বলা হয় রেস কন্ডিশনRace Conditionএকাধিক প্রসেস/থ্রেড শেয়ার্ড ডেটা একসাথে অ্যাক্সেস করলে এবং চূড়ান্ত ফলাফল তাদের এক্সিকিউশনের timing/ordering-এর ওপর নির্ভর করলে যে পরিস্থিতি তৈরি হয়।। এটি কোনো তাত্ত্বিক সমস্যা নয় — প্রতিদিন লেখা কনকারেন্ট কোডে এটি সবচেয়ে সাধারণ ও বিপজ্জনক বাগগুলোর একটি, কারণ এটি সবসময় ঘটে না (টাইমিং-নির্ভর), তাই টেস্টিংয়েও প্রায়ই ধরা পড়ে না।
২ · ক্রিটিক্যাল সেকশন — সংজ্ঞা ও কাঠামো
প্রতিটি প্রসেসের কোডে সেই নির্দিষ্ট অংশটুকু, যেখানে সে শেয়ার্ড ডেটা অ্যাক্সেস (পড়ে বা পরিবর্তন) করে, তাকে বলা হয় ক্রিটিক্যাল সেকশনCritical Sectionপ্রসেসের কোডের সেই অংশ যেখানে শেয়ার্ড ডেটা অ্যাক্সেস হয় — একসাথে মাত্র একটি প্রসেস এখানে থাকতে পারবে।। এই সমস্যা সমাধান করতে হলে প্রতিটি প্রসেসের কোডকে চারটি অংশে ভাগ করে ভাবা হয়:
entry section — ক্রিটিক্যাল সেকশনে ঢোকার অনুমতি চাওয়ার কোড (এখানেই lock/semaphore-এর মতো টুল বসে, L18 দেখুন)। critical section — শেয়ার্ড ডেটা অ্যাক্সেসের প্রকৃত কোড। exit section — বেরিয়ে যাওয়ার ঘোষণা (lock release)। remainder section — বাকি সব সাধারণ কোড, যা শেয়ার্ড ডেটা স্পর্শ করে না।
৩ · তিনটি প্রয়োজনীয় শর্ত
একটি সঠিক ক্রিটিক্যাল-সেকশন সমাধানকে অবশ্যই এই তিনটি শর্ত একসাথে পূরণ করতে হবে —
একই সময়ে দুইটি প্রসেস কখনোই তাদের নিজ নিজ ক্রিটিক্যাল সেকশনে থাকতে পারবে না — এটিই মূল শর্ত।
যদি কোনো প্রসেস তার ক্রিটিক্যাল সেকশনে না থাকে, তাহলে ঢুকতে চাওয়া কোনো প্রসেসকে সসীম সময়ের মধ্যে সিদ্ধান্ত নিতে পারতেই হবে — এমন প্রসেস দিয়ে সে আটকে থাকতে পারবে না যারা আদৌ ঢুকতেই চায় না।
একটি প্রসেস অনুরোধ করার পর, অন্য প্রসেসগুলো তাদের ক্রিটিক্যাল সেকশনে কতবার ঢুকতে পারবে তার একটি সীমা থাকতেই হবে — এটি স্টারভেশন ঠেকায়।
Mutual exclusion ভাঙলে সরাসরি রেস কন্ডিশন ফিরে আসে (নিচের কোড সেল ঠিক এটাই দেখাবে)। Progress ভাঙলে এমন হতে পারে যে কেউই ক্রিটিক্যাল সেকশনে ঢুকতে না পারা সত্ত্বেও ব্যবস্থাটি "লক" হয়ে যায় (deadlock-এর একটি রূপ, M6-এ বিস্তারিত)। Bounded waiting ভাঙলে একটি নির্দিষ্ট প্রসেস অনির্দিষ্টকালের জন্য অপেক্ষা করতে থাকতে পারে যদিও অন্যরা বারবার সুযোগ পাচ্ছে — এটি স্টারভেশন, যা M5-এর পরবর্তী পাঠগুলোতে (বিশেষত L20 readers-writers) আবার দেখা যাবে।
৪ · কোডে রেস কন্ডিশন — একটি "হারানো" increment
একটি সাধারণ shared_counter += 1 দেখতে একক ধাপের মনে হলেও, প্রসেসরের স্তরে এটি আসলে তিনটি
আলাদা সাব-স্টেপ: (১) বর্তমান মান read করা, (২) স্থানীয়ভাবে ১ add করা,
(৩) নতুন মান আবার শেয়ার্ড ভেরিয়েবলে write করা। যদি দুটি প্রসেস এই সাব-স্টেপগুলো একে অপরের সাথে
ইন্টারলিভড হয়ে চালায় (কোনো ক্রিটিক্যাল-সেকশন সুরক্ষা ছাড়া), তাহলে একটি increment হারিয়ে যেতে পারে। নিচের কোডে
আমরা এই ইন্টারলিভিং সরাসরি লিখেই দেখাচ্ছি (deliberately) — যাতে বাগটি প্রতিবার একইভাবে প্রজনন হয়, বাস্তব থ্রেডিং-এর
র্যান্ডম টাইমিং-এর ওপর নির্ভর না করে।
# একটি "shared_counter" -- P1 ও P2 দুজনেই এটি ২ বার করে বাড়াতে চায় (মোট প্রত্যাশিত বৃদ্ধি = 4)
# কিন্তু increment আসলে ৩টি সাব-স্টেপ -- read, add, write -- আমরা সেগুলো আলাদা ফাংশন হিসেবে লিখছি
shared_counter = 0
local = {"P1": None, "P2": None} # প্রতিটি প্রসেসের নিজস্ব "রেজিস্টার" (read করা মান)
def read_step(name):
global shared_counter
local[name] = shared_counter
print(f"{name}: read() -> shared_counter={shared_counter}, local[{name}]={local[name]}")
def add_step(name):
local[name] += 1
print(f"{name}: add(1) -> local[{name}]={local[name]}")
def write_step(name):
global shared_counter
shared_counter = local[name]
print(f"{name}: write() -> shared_counter={shared_counter}")
# ডেলিবারেটলি ইন্টারলিভড শিডিউল -- প্রথম রাউন্ডে P1 ও P2 একসাথে রেস করে (কোনো ক্রিটিক্যাল-সেকশন সুরক্ষা নেই)
schedule = [
("P1", read_step), ("P2", read_step), # দুজনেই একই stale মান পড়ে ফেলল
("P1", add_step), ("P2", add_step),
("P1", write_step),("P2", write_step), # P2-এর write, P1-এর write-কে ওভাররাইট করলো -- ১টি increment হারালো
# দ্বিতীয় রাউন্ড -- এবার কোনো ওভারল্যাপ নেই (পুরোপুরি সিকোয়েন্সিয়াল), তাই এই দুটো সঠিকভাবেই গণনা হবে
("P1", read_step), ("P1", add_step), ("P1", write_step),
("P2", read_step), ("P2", add_step), ("P2", write_step),
]
for name, step in schedule:
step(name)
expected = 4 # P1 ২ বার + P2 ২ বার ইনক্রিমেন্ট করার চেষ্টা করেছে
print()
print("ফাইনাল shared_counter =", shared_counter)
print("প্রত্যাশিত (রেস না হলে) =", expected)
print("লস্ট আপডেট হয়েছে!" if shared_counter < expected else "কোনো লস্ট আপডেট হয়নি।")
shared_counter=0 পড়ে ফেলে (একজন লেখার আগেই আরেকজন পড়ে
নিয়েছে), তারপর দুজনেই নিজের স্থানীয় মান ১ করে, এবং শেষে P2-এর write P1-এর write-কে ওভাররাইট করে দেয় —
ফলাফলে মোট ৪টি ইনক্রিমেন্টের চেষ্টা সত্ত্বেও কাউন্টার মাত্র ৩ পর্যন্ত পৌঁছায়। এটিই লস্ট আপডেট — রেস কন্ডিশনের
সবচেয়ে ক্লাসিক, বাস্তব পরিণতি।
এই সমস্যার মূল কারণ — read_step, add_step, write_step তিনটি একসাথে,
অবিভাজ্যভাবে (atomically) চলেনি; মাঝখানে অন্য প্রসেসকে ঢুকতে দেওয়া হয়েছে। যদি এই তিনটি ধাপ একটি ক্রিটিক্যাল
সেকশন হিসেবে সুরক্ষিত থাকত (একসাথে মাত্র একটি প্রসেস এই তিন ধাপ চালাতে পারত), তাহলে লস্ট আপডেট অসম্ভব হয়ে
যেত। L18-এ ঠিক এই সুরক্ষা তৈরির টুল — মিউটেক্স ও সেমাফোর — শিখব, এবং এই একই সিনারিওকে সঠিকভাবে ফিক্স করে দেখব।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ যদি শুধু একটিমাত্র প্রসেস/থ্রেড কখনোই কোনো শেয়ার্ড ডেটা অ্যাক্সেস না করত, তাহলে কি ক্রিটিক্যাল সেকশন প্রবলেমের কোনো দরকার থাকত?
না। ক্রিটিক্যাল সেকশন প্রবলেম শুধুই তখন প্রাসঙ্গিক যখন একাধিক প্রসেস/থ্রেড একই শেয়ার্ড ডেটায় অ্যাক্সেস করে (এবং অন্তত একটি সেই ডেটা পরিবর্তন করে)। যদি প্রতিটি প্রসেস সম্পূর্ণ নিজস্ব, বিচ্ছিন্ন ডেটায় কাজ করত (কোনো শেয়ারিং নেই), তাহলে কোনো রেস কন্ডিশন হতেই পারত না — কারণ কারো কাজ অন্যের ডেটাকে স্পর্শ করছে না। শেয়ারিং-ই এই পুরো সমস্যার মূল কারণ (L08-এর শেয়ার্ড-মেমরি IPC-এর সরাসরি ট্রেড-অফ)।
প্র ০২ উপরের কোড সেলে দ্বিতীয় রাউন্ডে (P1-এর পুরো তিন ধাপ, তারপর P2-এর পুরো তিন ধাপ) কেন কোনো লস্ট আপডেট হয়নি?
দ্বিতীয় রাউন্ডে P1 তার read-add-write সম্পূর্ণ শেষ করার আগে P2 কোনো ধাপই শুরু করেনি — অর্থাৎ এখানে কোনো ইন্টারলিভিং নেই, দুটি অপারেশন সম্পূর্ণ সিকোয়েন্সিয়ালি (একের পর এক) ঘটেছে। P2 যখন read করে, ততক্ষণে P1-এর write ইতিমধ্যে সম্পন্ন হয়ে গেছে, তাই P2 সঠিক, আপ-টু-ডেট মান পড়ে। এটিই দেখায় — সমস্যাটা মাল্টিপল প্রসেস থাকাতে নয়, বরং তাদের সাব-স্টেপগুলো ওভারল্যাপ করাতে।
প্র ০৩ Progress শর্তটি "bounded waiting" থেকে ঠিক কীভাবে আলাদা? একটি উদাহরণ ভাবুন যেখানে progress আছে কিন্তু bounded waiting নেই।
Progress নিশ্চিত করে যে সিদ্ধান্তটি (কে ঢুকবে) অসীম সময় ধরে ঝুলে থাকবে না এবং শুধু আগ্রহী প্রসেসগুলোই এতে অংশ নেবে। Bounded waiting আরও কঠোর — এটি নিশ্চিত করে একটি নির্দিষ্ট প্রসেসের অনুরোধের পর অন্যরা সীমিত সংখ্যকবারই তার আগে ঢুকতে পারবে। কল্পনা করুন এমন একটি স্কিম যেখানে প্রতিবার সিদ্ধান্ত নেওয়া হয় ঠিকঠাক (progress আছে), কিন্তু নিয়মটা সবসময় "সর্বোচ্চ প্রায়োরিটির" প্রসেসকে বেছে নেয় — একটি কম-প্রায়োরিটি প্রসেস তাত্ত্বিকভাবে চিরকাল হারতে পারে যদি উচ্চ-প্রায়োরিটি প্রসেস বারবার আসতেই থাকে। এখানে progress আছে (কেউ না কেউ ঢুকছে), কিন্তু bounded waiting নেই (একটি নির্দিষ্ট প্রসেসের জন্য কোনো সীমা নেই) — এটিই স্টারভেশন, যা L11-এর প্রায়োরিটি শিডিউলিং-এও দেখা গিয়েছিল।
অনুশীলন
-
পরীক্ষা করুন: উপরের কোড সেলে
schedule-এর প্রথম ৬টি ধাপকে (P1, P2, P1, P2, P1, P2 এর read-add-write ইন্টারলিভিং) সম্পূর্ণ সিকোয়েন্সিয়াল করে দিন (P1-এর তিন ধাপ, তারপর P2-এর তিন ধাপ) এবং Run চেপে দেখুন ফাইনাল কাউন্টার কী হয়।সম্পূর্ণ সিকোয়েন্সিয়াল করলে (কোনো ইন্টারলিভিং নেই), শুরু থেকে শেষ পর্যন্ত মোট ৪টি increment-ই সঠিকভাবে গণনা হবে, এবং ফাইনাল
shared_counterহবে ঠিক ৪ (আর কোনো "লস্ট আপডেট হয়েছে!" বার্তা আসবে না)। এটি নিশ্চিত করে — সমস্যাটা প্রসেসের সংখ্যা নয়, বরং তাদের সাব-স্টেপের ওভারল্যাপ। -
চিন্তা করুন: যদি আরও একটি তৃতীয় প্রসেস P3 একই সময়ে
shared_counter-এ শুধু read করে (কখনো write করে না), তাহলে কি সেটাও রেস কন্ডিশনের অংশ হবে?সাধারণত না, যদি P3 শুধুই read করে এবং কখনো লেখে না, তাহলে P3 নিজে কোনো লস্ট আপডেট তৈরি করবে না (কারণ সে কারো লেখাকে ওভাররাইট করছে না)। তবে P3 এখনও একটি অসামঞ্জস্যপূর্ণ মান পড়তে পারে (যেমন এমন এক মুহূর্তে পড়া যখন আরেকটি প্রসেস মাঝ-আপডেটে আছে) — এটি রেস কন্ডিশন না হলেও একটি ভিন্ন সমস্যা (stale/inconsistent read), যা মাল্টি-ফিল্ড শেয়ার্ড ডেটাতে (যেমন একাধিক ভেরিয়েবল একসাথে আপডেট করার সময়) আরও গুরুতর হয়ে ওঠে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরের পাঠ: মিউটেক্স লক ও সেমাফোর L18 ঠিক এই লস্ট-আপডেট সমস্যাটি বাস্তবে কীভাবে সমাধান করা হয় — একটি real semaphore ক্লাস লিখে।
- পূর্বের পাঠ ফিরে দেখুন: IPC — শেয়ার্ড মেমরি ও মেসেজ পাসিং L08 শেয়ার্ড-মেমরি IPC কেন কোঅর্ডিনেশন দাবি করে, তার শুরুর ব্যাখ্যা।
- Cybersecurity কোর্স সঙ্গী কোর্স রেস কন্ডিশনের একটি নিরাপত্তা-কেন্দ্রিক রূপ — TOCTOU (Time-of-Check to Time-of-Use) বাগ — নিরাপত্তা দৃষ্টিকোণ থেকে দেখতে সেই কোর্সে যান।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ M5-এর বাকি পাঠগুলো — সেমাফোর, ক্লাসিক সিনক্রোনাইজেশন সমস্যা ও মনিটর।