Mutex (انحصار متقابل) — یک اولیه همگامسازی است که تضمین میکند تنها یک thread میتواند بخش بحرانی کد را در هر لحظه اجرا کند. به گفته Microsoft Docs (Synchronization Objects, 2024)، اصل اساسی Mutex مالکیت است: threadای که Mutex را تصاحب میکند مالک آن میشود و آن را فقط هنگام خروج از بخش بحرانی آزاد میکند. Mutex — ابزاری اساسی برای جلوگیری از Race Condition و تضمین یکپارچگی دادهها در برنامههای چندنخی است.
نکات اصلی
Mutex (مخفف Mutual Exclusion — انحصار متقابل) — یک شیء همگامسازی است که دسترسی به منبع مشترک را در محیط چندنخی مدیریت میکند. وقتی thread وارد بخش بحرانی میشود، Mutex را تصاحب میکند. اگر thread دیگری بخواهد همان Mutex را تصاحب کند، تا زمان آزاد شدن قفل توسط thread اول به حالت انتظار میرود.
معماری Mutex به سیستم عامل THE برمیگردد که توسط Edsger Dijkstra در سال 1965 توسعه یافت. این Dijkstra بود که مفهوم semaphoreها را معرفی کرد که بعداً Mutex به عنوان یک حالت خاص از آن جدا شد — semaphore باینری با پشتیبانی از مالکیت. سیستمعاملهای مدرن (Linux، Windows، Android) Mutex را در سطح هسته پیادهسازی میکنند که همگامسازی صحیح حتی بین فرآیندهای مختلف را تضمین میکند.
ویژگی کلیدی Mutex ownership (مالکیت) است. فقط threadای که muteks را تصاحب کرده میتواند آن را آزاد کند. این موضوع Mutex را از semaphore باینری متمایز میکند، جایی که هر threadای میتواند سیگنال (عملیات V) را اجرا کند. مالکیت از آزادسازی تصادفی قفل توسط thread دیگر جلوگیری میکند و Mutex را برای سناریوهای معمول همگامسازی در توسعه موبایل ایمنتر میکند. طبق Android Developer Docs (Processes and Threads, 2024)، استفاده از Mutex به جای synchronized میتواند عملکرد را تا 30% در رقابت بالا افزایش دهد.
Mutex در یکی از دو حالت قرار دارد: قفل شده (locked) — تصاحب شده توسط thread؛ آزاد (unlocked) — تصاحب نشده. دو عملیات پایه — lock() (تصاحب) و unlock() (آزادسازی). اگر Mutex از قبل تصاحب شده باشد، thread فراخواننده lock() تا زمان آزاد شدن مسدود میشود. در JVM thread مسدود شده به حالت BLOCKED میرود و CPU مصرف نمیکند.
وقتی Mutex آزاد میشود، سیستم انتخاب میکند کدام یک از threadهای منتظر قفل را دریافت کند. در برنامهریزی ناعادلانه (non-fair) انتخاب ممکن است به threadی برسد که تازه muteks را آزاد کرده است — این کار توان عملیاتی را افزایش میدهد اما میتواند به Starvation (گرسنگی) منجر شود. برنامهریز عادلانه (fair) از صف FIFO استفاده میکند: اولین thread منتظر اولین قفل را دریافت میکند. ReentrantLock(true) دقیقاً همین مکانیزم را پیادهسازی میکند.
اکثر پیادهسازیهای Mutex در Java/Kotlin از تصاحب بازگشتی (reentrant) پشتیبانی میکنند. اگر thread از قبل مالک Mutex است و دوباره lock() را فراخوانی میکند، عملیات موفق است — Mutex خود را مسدود نمیکند. شمارنده بازگشت افزایش مییابد و thread باید unlock() را به همان تعداد lock() فراخوانی کند. این برای فراخوانیهای بازگشتی و بخشهای بحرانی تودرتو مهم است.
یک کار معمول را در نظر بگیرید — محافظت از شمارنده مشترک در برابر Race Condition با ReentrantLock (Mutex کلاسیک در Java/Kotlin). بدون Mutex کد نتیجه نادرستی میداد؛ با Mutex هر 1000 thread مقدار شمارنده را تضمین شده افزایش میدهند.
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 برای همیشه قفل میماند — این به Deadlock منجر میشود. بلوک finally آزادسازی Mutex را در هر نتیجه اجرای بخش تضمین میکند.
رویکرد جایگزین در Kotlin — استفاده از تابع توسعهای withLock که به طور خودکار lock/unlock را با finally مدیریت میکند.
fun increment() {
mutex.withLock { // lock + try/finally به طور خودکار
count++
}
}
fun getCount(): Int = mutex.withLock { count }
این سه مکانیزم همگامسازی اغلب اشتباه گرفته میشوند، اگرچه ویژگیها و حوزههای کاربرد متفاوتی دارند. Mutex — باینری، با مالکیت. Semaphore — شمارنده مجوزها، بدون مالکیت. Monitor — مکانیزم سطح بالا که Mutex را با متغیرهای شرطی (condition variables) ترکیب میکند. درک تفاوتها برای انتخاب ابزار مناسب برای کار خاص حیاتی است.
| پارامتر | Mutex | Semaphore | Monitor |
|---|---|---|---|
| نوع | باینری (0/1) | شمارنده (0..N) | باینری + شرایط |
| مالکیت | فقط مالک میتواند unlock کند | هر threadی میتواند signal کند | فقط مالک |
| بازگشتی بودن | معمولاً بله (reentrant) | خیر | بله |
| انتظار شرطی | خیر (نیاز به Condition) | خیر | داخلی (wait/notify) |
| مثال در Java/Kotlin | ReentrantLock | Semaphore(permits) | synchronized |
چه زمانی Mutex انتخاب کنیم: باید از یک منبع در برابر دسترسی همزمان محافظت کرد — مثلاً مجموعه مشترک، فایل یا شمارنده. چه زمانی Semaphore انتخاب کنیم — باید تعداد دسترسیهای همزمان به استخر منابع را محدود کرد، مثلاً استخر اتصالات پایگاه داده با 5 اتصال. چه زمانی Monitor انتخاب کنیم — همگامسازی با انتظار شرطی نیاز است، مثلاً صف تولیدکننده-مصرفکننده از طریق wait/notify. در توسعه مدرن Android، synchronized اغلب با ReentrantLock یا kotlinx.coroutines Mutex جایگزین میشود.
رایجترین خطا — عدم وجود بلوک finally برای فراخوانی unlock(). اگر در بخش بحرانی استثنا رخ دهد، Mutex قفل میماند و سایر threadها برای همیشه منتظر میمانند. حتی اگر مطمئنید که استثنا غیرممکن است — همیشه از try/finally یا withLock استفاده کنید. این اصل defensive programming است که به ویژه در توسعه موبایل مهم است، جایی که استثناها ممکن است به دلیل کمبود حافظه یا Configuration Changes ایجاد شوند.
وقتی در برنامه از چندین Mutex استفاده میشود، تعیین ترتیب یکسان تصاحب آنها حیاتی است. اگر Thread A M1 → M2 را تصاحب کند و Thread B M2 → M1 را تصاحب کند، Deadlock رخ میدهد. در پروژههای بزرگ (بیش از 50 هزار خط کد) ترتیب قفلها در تصمیم معماری مستند شده و توسط linterها بررسی میشود. ابزار Lock Checker در IntelliJ IDEA به طور خودکار ترتیب ناسازگار تصاحب قفلها را تشخیص میدهد.
نگهداشتن Mutex بیشتر از 1-2 میلیثانیه — نشانه طراحی نادرست است. بخش بحرانی باید فقط شامل حداقل عملیات ضروری باشد. درخواستهای شبکه، ورودی-خروجی فایل و محاسبات پیچیده باید خارج از بلوک قفل شده اجرا شوند. در Android، نگهداشتن طولانی قفل در thread UI منجر به افت فریم (jank) و ANR میشود. اگر بخش بحرانی عمدتاً از عملیات خواندن تشکیل شده است، از ReadWriteLock استفاده کنید.
کتابخانه kotlinx.coroutines پیادهسازی خاص خود از Mutex را ارائه میدهد که اساساً با ReentrantLock کلاسیک متفاوت است. تفاوت اصلی — suspending Mutex thread سیستمعامل را مسدود نمیکند، بلکه coroutine را تا زمان آزاد شدن قفل معلق میکند. این بدان معناست که thread میتواند coroutineهای دیگر را اجرا کند در حالی که coroutine جاری منتظر 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 — thread را مسدود نمیکند
count++
}
}
suspend fun getCount(): Int = mutex.withLock { count }
}
ویژگیهای کلیدی kotlinx Mutex: غیربازگشتی بودن (non-reentrant) — برخلاف ReentrantLock، coroutine نمیتواند Mutexی را که از قبل در اختیار دارد دوباره تصاحب کند. اگر این لازم است، به جای Mutex از Semaphore(1) استفاده کنید. علاوه بر این، Mutex از kotlinx.coroutines غیرمسدودکننده است: از تعلیق از طریق suspend استفاده میکند که امکان مسدود نکردن thread استخر را فراهم میکند.
در عمل suspending Mutex به دو دلیل بر ReentrantLock کلاسیک در کد coroutine ترجیح داده میشود: مقیاسپذیری — یک coroutine منتظر Mutex است و thread به coroutineهای دیگر خدمات میدهد که توان عملیاتی سیستم را افزایش میدهد; عدم وجود BlockedThread — منابع برای نگهداری پشته thread مسدود شده هدر نمیرود. طبق JetBrains (Kotlin Coroutines Guide, 2024)، استفاده از suspending Mutex توان عملیاتی را تا 40% با 100+ corotine افزایش میدهد.
سوالات متداول
مالکیت (ownership) — تفاوت اساسی. Mutex به خاطر میسپارد کدام thread آن را تصاحب کرده و فقط آن thread میتواند آن را آزاد کند. semaphore باینری (Semaphore(1)) مالک ندارد — هر threadی میتواند release() را اجرا کند. بنابراین Mutex ایمنتر است: thread دیگر نمیتواند به طور تصادفی قفل دیگران را آزاد کند، اما semaphore میتواند.
synchronized سادهتر و کوتاهتر است — از آن برای بخشهای بحرانی ساده بدون محدودیت زمانی و بدون کنترل عدالت استفاده کنید. از ReentrantLock زمانی استفاده کنید که TryLock با محدودیت زمانی، fair-planning، Condition Variables یا قطع کردن thread منتظر (lockInterruptibly) نیاز دارید. برای coroutineها همیشه از kotlinx.coroutines.sync.Mutex استفاده کنید.
Spinlock — قفلی است که در آن thread نمیخوابد، بلکه در یک حلقه (spin) وضعیت قفل را بررسی میکند. Spinlock CPU مصرف میکند اما زمینه را تغییر نمیدهد، که آن را برای بخشهای بحرانی کوتاه (تا 10 دستورالعمل) مفید میکند. Mutex thread را به حالت BLOCKED میبرد که به دلیل تغییر زمینه 10-50 میکروثانیه گرانتر است اما CPU مصرف نمیکند.
در سطح هسته Linux، Mutex از طریق futex (fast userspace mutex) پیادهسازی شده است. thread ابتدا سعی میکند قفل را در userspace از طریق دستور اتمی CAS (Compare-And-Swap) تصاحب کند. اگر Mutex آزاد باشد — تصاحب بدون syscall انجام میشود. اگر مشغول باشد — thread syscall futex(FUTEX_WAIT) را انجام میدهد و میخوابد. هنگام آزادسازی، syscall futex(FUTEX_WAKE) یک thread منتظر را بیدار میکند.
بله، Mutexهای بین فرآیندی (inter-process mutex) وجود دارند. در Windows این Named Mutex است، در Linux — pthread_mutexattr_setpshared با ویژگی PTHREAD_PROCESS_SHARED. در Android Bionic libc نیز از Mutexهای بین فرآیندی از طریق توصیفکنندههای فایل پشتیبانی میکند. Mutexهای بین فرآیندی برای همگامسازی بین برنامههای مختلف یا بین یک فرآیند و فرآیندهای فرزند آن استفاده میشوند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید