Mutex (পারস্পরিক বর্জন) একটি সিঙ্ক্রোনাইজেশন প্রিমিটিভ যা গ্যারান্টি দেয় যে যেকোনো সময় শুধুমাত্র একটি থ্রেড কোডের ক্রিটিক্যাল সেকশন এক্সিকিউট করতে পারে। Microsoft Docs (Synchronization Objects, 2024) অনুসারে, Mutex-এর মূল নীতি হল মালিকানা: যে থ্রেড Mutex ক্যাপচার করে সে তার মালিক হয়ে যায় এবং ক্রিটিক্যাল সেকশন থেকে বের হওয়ার সময়ই এটি প্রকাশ করে। Mutex রেস কন্ডিশন (Race Condition) প্রতিরোধ এবং মাল্টিথ্রেডেড অ্যাপ্লিকেশনে ডেটা অখণ্ডতা নিশ্চিত করার জন্য একটি মৌলিক টুল।
মূল বিষয়
Mutex (Mutual Exclusion — পারস্পরিক বর্জনের সংক্ষিপ্ত রূপ) একটি সিঙ্ক্রোনাইজেশন অবজেক্ট যা মাল্টিথ্রেডেড পরিবেশে শেয়ার্ড রিসোর্সে অ্যাক্সেস পরিচালনা করে। যখন একটি থ্রেড ক্রিটিক্যাল সেকশনে প্রবেশ করে, এটি Mutex ক্যাপচার করে। যদি অন্য একটি থ্রেড একই Mutex ক্যাপচার করার চেষ্টা করে, তবে এটি প্রথম থ্রেড দ্বারা লক প্রকাশ না হওয়া পর্যন্ত অপেক্ষার অবস্থায় রাখা হয়।
Mutex-এর আর্কিটেকচার THE অপারেটিং সিস্টেমের সাথে সম্পর্কিত, যা Edsger Dijkstra 1965 সালে ডিজাইন করেছিলেন। Dijkstra সেমাফোরের ধারণা প্রবর্তন করেছিলেন, যেখান থেকে পরে Mutex একটি বিশেষ ক্ষেত্রে পরিণত হয়েছিল — মালিকানা সমর্থন সহ একটি বাইনারি সেমাফোর। আধুনিক OS (Linux, Windows, Android) কার্নেল স্তরে Mutex প্রয়োগ করে, যা বিভিন্ন প্রক্রিয়ার মধ্যেও সঠিক সিঙ্ক্রোনাইজেশন নিশ্চিত করে।
Mutex-এর মূল বৈশিষ্ট্য হল মালিকানা (ownership)। শুধুমাত্র যে থ্রেড মিউটেক্স ক্যাপচার করেছে সেটিই এটি প্রকাশ করতে পারে। এটি Mutex-কে বাইনারি সেমাফোর থেকে আলাদা করে, যেখানে যেকোনো থ্রেড সিগন্যাল (V-অপারেশন) করতে পারে। মালিকানা অন্য থ্রেড দ্বারা আকস্মিক লক প্রকাশ রোধ করে, যা মোবাইল ডেভেলপমেন্টে সাধারণ সিঙ্ক্রোনাইজেশন দৃশ্যপটের জন্য Mutex-কে আরও নিরাপদ করে তোলে। Android Developer Docs (Processes and Threads, 2024) অনুসারে, উচ্চ প্রতিযোগিতায় synchronized-এর পরিবর্তে Mutex ব্যবহার করলে কর্মক্ষমতা 30% পর্যন্ত উন্নত হতে পারে।
Mutex দুটি অবস্থার একটিতে থাকে: লকড (locked) — থ্রেড দ্বারা ক্যাপচার করা; বা আনলকড (unlocked) — ক্যাপচার করা হয়নি। দুটি মৌলিক অপারেশন রয়েছে: lock() (ক্যাপচার) এবং unlock() (মুক্ত করা)। যদি Mutex ইতিমধ্যে লকড থাকে, lock() কল করা থ্রেডটি লক মুক্ত না হওয়া পর্যন্ত ব্লক হয়ে যায়। JVM-এ, ব্লক করা থ্রেডটি BLOCKED অবস্থায় চলে যায় এবং CPU গ্রাস করে না।
যখন Mutex মুক্ত হয়, সিস্টেম নির্বাচন করে কোন অপেক্ষারত থ্রেড লক পাবে। অ-ন্যায্য (non-fair) শিডিউলিং-এ, পছন্দটি সেই থ্রেডের উপর পড়তে পারে যেটি এইমাত্র মিউটেক্স মুক্ত করেছে — এটি থ্রুপুট বাড়ায় কিন্তু স্টার্ভেশন (অনাহার) হতে পারে। একটি ন্যায্য (fair) শিডিউলার FIFO কিউ ব্যবহার করে: প্রথম অপেক্ষারত থ্রেড প্রথমে লক পায়। ReentrantLock(true) ঠিক এই পদ্ধতি প্রয়োগ করে।
Java/Kotlin-এ অধিকাংশ Mutex বাস্তবায়ন রিইন্ট্রান্ট ক্যাপচার সমর্থন করে। যদি একটি থ্রেড ইতিমধ্যে Mutex-এর মালিক হয় এবং আবার lock() কল করে, অপারেশনটি সফল হয় — Mutex নিজেকে ব্লক করে না। পুনরাবৃত্তি কাউন্টার বেড়ে যায়, এবং থ্রেডকে lock()-এর যতবার কল করা হয়েছে ততবার unlock() কল করতে হবে। এটি পুনরাবৃত্ত কল এবং নেস্টেড ক্রিটিক্যাল সেকশনের জন্য গুরুত্বপূর্ণ।
একটি সাধারণ কাজ বিবেচনা করুন — ReentrantLock (Java/Kotlin-এ ক্লাসিক Mutex) ব্যবহার করে একটি শেয়ার্ড কাউন্টারকে রেস কন্ডিশন থেকে রক্ষা করা। Mutex ছাড়া, কোডটি ভুল ফলাফল দেবে; Mutex-এর সাথে, সমস্ত 1000 থ্রেড নির্ভরযোগ্যভাবে কাউন্টারের মান বৃদ্ধি করে।
import java.util.concurrent.locks.ReentrantLock
class MutexCounter {
private val mutex = ReentrantLock()
private var count = 0
fun increment() {
mutex.lock()
try {
count++ // ক্রিটিক্যাল সেকশন
} finally {
mutex.unlock() // বাধ্যতামূলক finally
}
}
fun getCount(): Int {
mutex.lock()
try {
return count
} finally {
mutex.unlock()
}
}
}
fun main() = runBlocking {
val counter = MutexCounter()
val jobs = List(1000) {
launch(Dispatchers.Default) {
counter.increment()
}
}
jobs.forEach { it.join() }
println(counter.getCount()) // সর্বদা 1000
}
finally ব্লক-এ মনোযোগ দিন — Mutex-এর সাথে কাজ করার সময় একটি বাধ্যতামূলক প্যাটার্ন। যদি ক্রিটিক্যাল সেকশনের ভিতরে কোনো ব্যতিক্রম ঘটে, unlock() কল হবে না এবং Mutex চিরতরে লকড থাকবে — এটি ডেডলকের দিকে নিয়ে যায়। finally ব্লক সেকশন এক্সিকিউশন যেভাবেই শেষ হোক না কেন Mutex মুক্ত করার গ্যারান্টি দেয়।
Kotlin-এ একটি বিকল্প পদ্ধতি হল withLock এক্সটেনশন ফাংশন ব্যবহার করা, যা finally-এর সাথে স্বয়ংক্রিয়ভাবে lock/unlock পরিচালনা করে।
fun increment() {
mutex.withLock { // lock + try/finally স্বয়ংক্রিয়ভাবে
count++
}
}
fun getCount(): Int = mutex.withLock { count }
এই তিনটি সিঙ্ক্রোনাইজেশন পদ্ধতি প্রায়ই বিভ্রান্ত হয়, যদিও এদের ভিন্ন বৈশিষ্ট্য এবং ব্যবহারের ক্ষেত্র রয়েছে। Mutex মালিকানা সহ বাইনারি। সেমাফোর মালিকানা ছাড়া একটি অনুমতি কাউন্টার। Monitor একটি উচ্চ-স্তরের পদ্ধতি যা Mutex-কে কন্ডিশন ভেরিয়েবলের সাথে একত্রিত করে। পার্থক্যগুলি বোঝা একটি নির্দিষ্ট কাজের জন্য সঠিক টুল নির্বাচনের জন্য অত্যন্ত গুরুত্বপূর্ণ।
| প্যারামিটার | Mutex | সেমাফোর | Monitor |
|---|---|---|---|
| প্রকার | বাইনারি (0/1) | গণনাযোগ্য (0..N) | বাইনারি + শর্ত |
| মালিকানা | শুধুমাত্র মালিক unlock করতে পারে | যেকোনো থ্রেড signal করতে পারে | শুধুমাত্র মালিক |
| পুনরাবৃত্তি | সাধারণত হ্যাঁ (reentrant) | না | হ্যাঁ |
| শর্তসাপেক্ষ অপেক্ষা | না (Condition প্রয়োজন) | না | অন্তর্নির্মিত (wait/notify) |
| Java/Kotlin-এ উদাহরণ | ReentrantLock | Semaphore(permits) | synchronized |
কখন Mutex নির্বাচন করবেন: আপনাকে একটি একক রিসোর্সকে সমবর্তী অ্যাক্সেস থেকে রক্ষা করতে হবে — উদাহরণস্বরূপ, একটি শেয়ার্ড কালেকশন, ফাইল বা কাউন্টার। কখন সেমাফোর নির্বাচন করবেন — আপনাকে একটি রিসোর্স পুলে সমবর্তী অ্যাক্সেসের সংখ্যা সীমিত করতে হবে, যেমন 5টি সংযোগ সহ একটি ডাটাবেস সংযোগ পুল। কখন Monitor নির্বাচন করবেন — আপনার শর্তসাপেক্ষ অপেক্ষা সহ সিঙ্ক্রোনাইজেশন প্রয়োজন, যেমন wait/notify-এর মাধ্যমে প্রযোজক-ভোক্তা কিউ। আধুনিক Android ডেভেলপমেন্টে, synchronized প্রায়ই ReentrantLock বা kotlinx.coroutines Mutex দ্বারা প্রতিস্থাপিত হয়।
সবচেয়ে সাধারণ ভুল হল unlock() কল করার জন্য finally ব্লকের অনুপস্থিতি। যদি ক্রিটিক্যাল সেকশনে কোনো ব্যতিক্রম ঘটে, Mutex লকড থাকে এবং অন্যান্য থ্রেড চিরকাল অপেক্ষা করে। এমনকি যদি আপনি নিশ্চিত হন যে ব্যতিক্রম অসম্ভব — সর্বদা try/finally বা withLock ব্যবহার করুন। এটি প্রতিরক্ষামূলক প্রোগ্রামিংয়ের একটি নীতি, বিশেষ করে মোবাইল ডেভেলপমেন্টে গুরুত্বপূর্ণ যেখানে মেমোরির অভাব বা Configuration Changes-এর কারণে ব্যতিক্রম উঠতে পারে।
যখন একটি অ্যাপ্লিকেশন একাধিক Mutex ব্যবহার করে, তখন একটি সামঞ্জস্যপূর্ণ ক্যাপচার ক্রম স্থাপন করা অত্যন্ত গুরুত্বপূর্ণ। যদি থ্রেড A, M1 → M2 ক্যাপচার করে এবং থ্রেড B, M2 → M1 ক্যাপচার করে, তাহলে ডেডলক হয়। বড় প্রকল্পে (50 হাজারের বেশি কোড লাইন), লকের ক্রম আর্কিটেকচার সিদ্ধান্তে ডকুমেন্ট করা হয় এবং লিন্টার দ্বারা যাচাই করা হয়। IntelliJ IDEA-তে Lock Checker টুল স্বয়ংক্রিয়ভাবে অসামঞ্জস্যপূর্ণ লক ক্যাপচার ক্রম সনাক্ত করে।
Mutex 1-2 মিলিসেকেন্ডের বেশি ধরে রাখা খারাপ ডিজাইনের লক্ষণ। ক্রিটিক্যাল সেকশনে শুধুমাত্র ন্যূনতম প্রয়োজনীয় অপারেশন থাকা উচিত। নেটওয়ার্ক অনুরোধ, ফাইল I/O এবং জটিল গণনা লক করা ব্লকের বাইরে সম্পাদন করা উচিত। Android-এ, UI থ্রেডে দীর্ঘ সময় লক ধরে রাখলে ফ্রেম ড্রপ (jank) এবং ANR হয়। যদি ক্রিটিক্যাল সেকশন প্রধানত পড়ার অপারেশন নিয়ে গঠিত হয়, তাহলে ReadWriteLock ব্যবহার করুন।
kotlinx.coroutines লাইব্রেরি তার নিজস্ব Mutex বাস্তবায়ন প্রদান করে, যা ক্লাসিক ReentrantLock থেকে মৌলিকভাবে ভিন্ন। মূল পার্থক্য হল suspending Mutex OS থ্রেডকে ব্লক করে না বরং লক মুক্ত না হওয়া পর্যন্ত করুটিনকে স্থগিত করে। এর মানে হল থ্রেড অন্যান্য করুটিন এক্সিকিউট করতে পারে যখন বর্তমান করুটিন Mutex-এর জন্য অপেক্ষা করছে।
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class CoroutineCounter {
private val mutex = Mutex()
private var count = 0
suspend fun increment() {
mutex.withLock { // suspending — থ্রেড ব্লক করে না
count++
}
}
suspend fun getCount(): Int = mutex.withLock { count }
}
kotlinx Mutex-এর মূল বৈশিষ্ট্য: অ-রিইন্ট্রান্ট (non-reentrant) — ReentrantLock-এর বিপরীতে, একটি করুটিন পুনরায় সেই Mutex ক্যাপচার করতে পারে না যার এটি ইতিমধ্যে মালিক। যদি এটি প্রয়োজন হয়, Mutex-এর পরিবর্তে Semaphore(1) ব্যবহার করুন। অতিরিক্তভাবে, kotlinx.coroutines-এর Mutex নন-ব্লকিং: এটি suspend-এর মাধ্যমে স্থগিতকরণ ব্যবহার করে, যা এটিকে পুল থ্রেড ব্লক করতে দেয় না।
ব্যবহারিকভাবে, করুটিন কোডে suspending Mutex দুটি কারণে ক্লাসিক ReentrantLock-এর চেয়ে পছন্দনীয়: স্কেলেবিলিটি — একটি করুটিন Mutex-এর জন্য অপেক্ষা করে যখন থ্রেড অন্যান্য করুটিন সেবা দেয়, সিস্টেম থ্রুপুট বাড়ায়; কোনো BlockedThread নেই — ব্লক করা থ্রেডের স্ট্যাক সংরক্ষণে রিসোর্স ব্যয় হয় না। JetBrains (Kotlin Coroutines Guide, 2024) অনুসারে, 100+ করুটিনের সাথে suspending Mutex ব্যবহার করলে থ্রুপুট 40% পর্যন্ত বৃদ্ধি পায়।
প্রায়শই জিজ্ঞাসিত প্রশ্ন
মালিকানা (ownership) হল মৌলিক পার্থক্য। Mutex মনে রাখে কোন থ্রেড এটি ক্যাপচার করেছে, এবং শুধুমাত্র সেই থ্রেড এটি প্রকাশ করতে পারে। একটি বাইনারি সেমাফোরের (Semaphore(1)) কোনো মালিক নেই — যেকোনো থ্রেড release() কল করতে পারে। তাই Mutex বেশি নিরাপদ: অন্য থ্রেড দুর্ঘটনাক্রমে অন্যের লক প্রকাশ করতে পারে না, কিন্তু সেমাফোর পারে।
synchronized সহজ এবং ছোট — টাইমআউট এবং ন্যায্যতা নিয়ন্ত্রণ ছাড়া সাধারণ ক্রিটিক্যাল সেকশনের জন্য এটি ব্যবহার করুন। ReentrantLock ব্যবহার করুন যখন আপনার টাইমআউট সহ TryLock, ন্যায্য শিডিউলিং, কন্ডিশন ভেরিয়েবল বা অপেক্ষারত থ্রেডে বাধা দেওয়ার (lockInterruptibly) প্রয়োজন হয়। করুটিনের জন্য, সর্বদা kotlinx.coroutines.sync.Mutex ব্যবহার করুন।
স্পিনলক হল একটি লক যেখানে থ্রেড ঘুমায় না বরং লকের অবস্থা পরীক্ষা করে একটি লুপে ঘোরে (spin)। স্পিনলক CPU গ্রাস করে কিন্তু কনটেক্সট সুইচ করে না, যা এটিকে ছোট ক্রিটিক্যাল সেকশনের (10 টি নির্দেশ পর্যন্ত) জন্য লাভজনক করে। Mutex থ্রেডকে BLOCKED অবস্থায় রাখে, যা কনটেক্সট সুইচিং-এর কারণে 10-50 মাইক্রোসেকেন্ড বেশি ব্যয়বহুল কিন্তু CPU নষ্ট করে না।
Linux কার্নেল স্তরে, Mutex futex (fast userspace mutex)-এর মাধ্যমে প্রয়োগ করা হয়। থ্রেড প্রথমে পারমাণবিক CAS (Compare-And-Swap) নির্দেশের মাধ্যমে ইউজারস্পেসে লক ক্যাপচার করার চেষ্টা করে। যদি Mutex মুক্ত থাকে — syscall ছাড়াই ক্যাপচার ঘটে। যদি ব্যস্ত থাকে — থ্রেড syscall futex(FUTEX_WAIT) করে এবং ঘুমিয়ে পড়ে। মুক্তির সময়, syscall futex(FUTEX_WAKE) একটি অপেক্ষারত থ্রেডকে জাগিয়ে তোলে।
হ্যাঁ, আন্তঃ-প্রক্রিয়াগত Mutex (inter-process mutex) বিদ্যমান। Windows-এ এটি Named Mutex, Linux-এ — PTHREAD_PROCESS_SHARED অ্যাট্রিবিউট সহ pthread_mutexattr_setpshared। Android-এর Bionic libc ফাইল ডিস্ক্রিপ্টরের মাধ্যমে আন্তঃ-প্রক্রিয়াগত Mutex সমর্থন করে। আন্তঃ-প্রক্রিয়াগত Mutex বিভিন্ন অ্যাপ্লিকেশনের মধ্যে বা একটি প্রক্রিয়া এবং তার চাইল্ড প্রক্রিয়াগুলির মধ্যে সিঙ্ক্রোনাইজেশনের জন্য ব্যবহৃত হয়।
সারসংক্ষেপ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন