Mutex در برنامه‌های موبایل — چیست، اصل کار و کاربرد انحصار متقابل

نویسنده: IT Sectr منتشر شده: 2026-03-18 زمان مطالعه: 10 دقیقه

Mutex (انحصار متقابل) — یک اولیه همگام‌سازی است که تضمین می‌کند تنها یک thread می‌تواند بخش بحرانی کد را در هر لحظه اجرا کند. به گفته Microsoft Docs (Synchronization Objects, 2024)، اصل اساسی Mutex مالکیت است: threadای که Mutex را تصاحب می‌کند مالک آن می‌شود و آن را فقط هنگام خروج از بخش بحرانی آزاد می‌کند. Mutex — ابزاری اساسی برای جلوگیری از Race Condition و تضمین یکپارچگی داده‌ها در برنامه‌های چندنخی است.

نکات اصلی

  • Mutex — مکانیزم انحصار متقابل که دسترسی به منبع را فقط برای یک thread در هر لحظه فراهم می‌کند
  • مالکیت (ownership) — ویژگی کلیدی Mutex: فقط threadای که قفل را تصاحب کرده می‌تواند آن را آزاد کند
  • برخلاف semaphore با شمارنده ≥2، Mutex فقط حالت 0 یا 1 دارد (semaphore باینری)
  • Deadlock با Mutex در ترتیب نادرست تصاحب چندین muteks رخ می‌دهد
  • suspending Mutex در Kotlin Coroutines thread سیستم‌عامل را مسدود نمی‌کند که آن را از ReentrantLock کلاسیک متمایز می‌کند

Mutex چیست؟

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 چگونه کار می‌کند

حالت‌ها و عملیات‌ها

Mutex در یکی از دو حالت قرار دارد: قفل شده (locked) — تصاحب شده توسط thread؛ آزاد (unlocked) — تصاحب نشده. دو عملیات پایه — lock() (تصاحب) و unlock() (آزادسازی). اگر Mutex از قبل تصاحب شده باشد، thread فراخواننده lock() تا زمان آزاد شدن مسدود می‌شود. در JVM thread مسدود شده به حالت BLOCKED می‌رود و CPU مصرف نمی‌کند.

برنامه‌ریزی threadهای منتظر

وقتی Mutex آزاد می‌شود، سیستم انتخاب می‌کند کدام یک از threadهای منتظر قفل را دریافت کند. در برنامه‌ریزی ناعادلانه (non-fair) انتخاب ممکن است به threadی برسد که تازه muteks را آزاد کرده است — این کار توان عملیاتی را افزایش می‌دهد اما می‌تواند به Starvation (گرسنگی) منجر شود. برنامه‌ریز عادلانه (fair) از صف FIFO استفاده می‌کند: اولین thread منتظر اولین قفل را دریافت می‌کند. ReentrantLock(true) دقیقاً همین مکانیزم را پیاده‌سازی می‌کند.

تصاحب بازگشتی (Reentrancy)

اکثر پیاده‌سازی‌های Mutex در Java/Kotlin از تصاحب بازگشتی (reentrant) پشتیبانی می‌کنند. اگر thread از قبل مالک Mutex است و دوباره lock() را فراخوانی می‌کند، عملیات موفق است — Mutex خود را مسدود نمی‌کند. شمارنده بازگشت افزایش می‌یابد و thread باید unlock() را به همان تعداد lock() فراخوانی کند. این برای فراخوانی‌های بازگشتی و بخش‌های بحرانی تودرتو مهم است.

مثال استفاده از Mutex در کد Kotlin

یک کار معمول را در نظر بگیرید — محافظت از شمارنده مشترک در برابر Race Condition با ReentrantLock (Mutex کلاسیک در Java/Kotlin). بدون Mutex کد نتیجه نادرستی می‌داد؛ با Mutex هر 1000 thread مقدار شمارنده را تضمین شده افزایش می‌دهند.

kotlin
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 مدیریت می‌کند.

kotlin
fun increment() {
    mutex.withLock {  // lock + try/finally به طور خودکار
        count++
    }
}

fun getCount(): Int = mutex.withLock { count }

Mutex در مقابل Semaphore در مقابل Monitor

این سه مکانیزم همگام‌سازی اغلب اشتباه گرفته می‌شوند، اگرچه ویژگی‌ها و حوزه‌های کاربرد متفاوتی دارند. Mutex — باینری، با مالکیت. Semaphore — شمارنده مجوزها، بدون مالکیت. Monitor — مکانیزم سطح بالا که Mutex را با متغیرهای شرطی (condition variables) ترکیب می‌کند. درک تفاوت‌ها برای انتخاب ابزار مناسب برای کار خاص حیاتی است.

پارامترMutexSemaphoreMonitor
نوعباینری (0/1)شمارنده (0..N)باینری + شرایط
مالکیتفقط مالک می‌تواند unlock کندهر threadی می‌تواند signal کندفقط مالک
بازگشتی بودنمعمولاً بله (reentrant)خیربله
انتظار شرطیخیر (نیاز به Condition)خیرداخلی (wait/notify)
مثال در Java/KotlinReentrantLockSemaphore(permits)synchronized

چه زمانی Mutex انتخاب کنیم: باید از یک منبع در برابر دسترسی همزمان محافظت کرد — مثلاً مجموعه مشترک، فایل یا شمارنده. چه زمانی Semaphore انتخاب کنیم — باید تعداد دسترسی‌های همزمان به استخر منابع را محدود کرد، مثلاً استخر اتصالات پایگاه داده با 5 اتصال. چه زمانی Monitor انتخاب کنیم — همگام‌سازی با انتظار شرطی نیاز است، مثلاً صف تولیدکننده-مصرف‌کننده از طریق wait/notify. در توسعه مدرن Android، synchronized اغلب با ReentrantLock یا kotlinx.coroutines Mutex جایگزین می‌شود.

خطاهای رایج در استفاده از Mutex

unlock فراموش شده در finally

رایج‌ترین خطا — عدم وجود بلوک finally برای فراخوانی unlock(). اگر در بخش بحرانی استثنا رخ دهد، Mutex قفل می‌ماند و سایر threadها برای همیشه منتظر می‌مانند. حتی اگر مطمئنید که استثنا غیرممکن است — همیشه از try/finally یا withLock استفاده کنید. این اصل defensive programming است که به ویژه در توسعه موبایل مهم است، جایی که استثناها ممکن است به دلیل کمبود حافظه یا Configuration Changes ایجاد شوند.

ترتیب مختلف تصاحب Mutex

وقتی در برنامه از چندین 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 استفاده کنید.

Mutex در Kotlin Coroutines

کتابخانه kotlinx.coroutines پیاده‌سازی خاص خود از Mutex را ارائه می‌دهد که اساساً با ReentrantLock کلاسیک متفاوت است. تفاوت اصلی — suspending Mutex thread سیستم‌عامل را مسدود نمی‌کند، بلکه coroutine را تا زمان آزاد شدن قفل معلق می‌کند. این بدان معناست که thread می‌تواند coroutineهای دیگر را اجرا کند در حالی که coroutine جاری منتظر Mutex است.

kotlin
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 افزایش می‌دهد.

سوالات متداول

تفاوت Mutex با semaphore باینری چیست؟

مالکیت (ownership) — تفاوت اساسی. Mutex به خاطر می‌سپارد کدام thread آن را تصاحب کرده و فقط آن thread می‌تواند آن را آزاد کند. semaphore باینری (Semaphore(1)) مالک ندارد — هر threadی می‌تواند release() را اجرا کند. بنابراین Mutex ایمن‌تر است: thread دیگر نمی‌تواند به طور تصادفی قفل دیگران را آزاد کند، اما semaphore می‌تواند.

چه زمانی از Mutex و چه زمانی از synchronized استفاده کنیم؟

synchronized ساده‌تر و کوتاه‌تر است — از آن برای بخش‌های بحرانی ساده بدون محدودیت زمانی و بدون کنترل عدالت استفاده کنید. از ReentrantLock زمانی استفاده کنید که TryLock با محدودیت زمانی، fair-planning، Condition Variables یا قطع کردن thread منتظر (lockInterruptibly) نیاز دارید. برای coroutineها همیشه از kotlinx.coroutines.sync.Mutex استفاده کنید.

Spinlock چیست و چه تفاوتی با Mutex دارد؟

Spinlock — قفلی است که در آن thread نمی‌خوابد، بلکه در یک حلقه (spin) وضعیت قفل را بررسی می‌کند. Spinlock CPU مصرف می‌کند اما زمینه را تغییر نمی‌دهد، که آن را برای بخش‌های بحرانی کوتاه (تا 10 دستورالعمل) مفید می‌کند. Mutex thread را به حالت BLOCKED می‌برد که به دلیل تغییر زمینه 10-50 میکروثانیه گران‌تر است اما CPU مصرف نمی‌کند.

Mutex در سطح سیستم‌عامل چگونه ساخته شده است؟

در سطح هسته 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 می‌تواند بین فرآیندی باشد؟

بله، Mutexهای بین فرآیندی (inter-process mutex) وجود دارند. در Windows این Named Mutex است، در Linux — pthread_mutexattr_setpshared با ویژگی PTHREAD_PROCESS_SHARED. در Android Bionic libc نیز از Mutexهای بین فرآیندی از طریق توصیف‌کننده‌های فایل پشتیبانی می‌کند. Mutexهای بین فرآیندی برای همگام‌سازی بین برنامه‌های مختلف یا بین یک فرآیند و فرآیندهای فرزند آن استفاده می‌شوند.

خلاصه

  • Mutex — اولیه انحصار متقابل که تضمین می‌کند فقط یک thread به طور همزمان بخش بحرانی را اجرا می‌کند
  • مالکیت (ownership) Mutex را از semaphore باینری متمایز می‌کند — فقط thread مالک می‌تواند آزاد کند
  • ReentrantLock در Java/Kotlin — پیاده‌سازی کلاسیک Mutex با پشتیبانی از تصاحب بازگشتی و TryLock
  • بلوک finally یا withLock برای جلوگیری از Deadlock در صورت استثنا ضروری است
  • suspending Mutex از kotlinx.coroutines thread سیستم‌عامل را مسدود نمی‌کند، coroutine را معلق می‌کند
  • ترتیب یکسان تصاحب چندین Mutex — تنها راه اجتناب از Deadlock در سیستم‌های پیچیده
  • بخش‌های بحرانی کوتاه (تا 1-2 میلی‌ثانیه) — کلید عملکرد برنامه‌های چندنخی بدون Starvation

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید