Race Condition در برنامه‌های موبایل: ماهیت، علل ایجاد و روش‌های پیشگیری

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

Race Condition وضعیتی در برنامه‌نویسی چندنخی است که نتیجه نهایی به ترتیب اجرای نخ‌ها بستگی دارد. بر اساس مستندات Oracle Java Tutorials (2024)، شرط رقابت هنگام دسترسی همزمان به یک منبع مشترک بدون همگام‌سازی ایجاد می‌شود. بدون مکانیزم‌های مناسب Race Condition منجر به خرابی داده‌ها و باگ‌های غیرقابل تکرار در برنامه‌های موبایل می‌شود.

نکات اصلی

  • Race Condition — نقصی در کد چندنخی که نتیجه اجرا به ترتیب نخ‌ها بستگی دارد
  • شرط رقابت هنگام عدم همگام‌سازی در دسترسی به منبع مشترک ایجاد می‌شود
  • رقابت داده — زیرمجموعه‌ای از Race Condition مرتبط با نوشتن و خواندن همزمان یک متغیر
  • Mutex و سمافورها — ابزارهای اصلی برای رفع شرط رقابت در توسعه موبایل
  • عملیات اتمی تقسیم‌ناپذیری اجرا را تضمین کرده و از رقابت نخ‌ها جلوگیری می‌کند

Race Condition چیست؟

Race Condition (شرط رقابت) — خطایی در برنامه چندنخی است که صحت عملکرد به ترتیب غیرقابل پیش‌بینی اجرای نخ‌ها بستگی دارد. هنگامی که دو یا چند نخ به طور همزمان بدون همگام‌سازی به یک منبع مشترک دسترسی پیدا می‌کنند، وضعیت نهایی منبع نامشخص می‌شود.

در توسعه موبایل Race Condition به ویژه خطرناک است زیرا نخ‌ها می‌توانند روی هسته‌های مختلف پردازنده با سرعت‌های متفاوت اجرا شوند. توسعه‌دهنده نمی‌تواند کنترل کند کدام نخ عملیات را اول به پایان می‌رساند — این تصمیم با برنامه‌ریز سیستم عامل است. بر اساس تحقیقات IBM (Concurrency Bugs in Android, 2022)، حدود 23% از باگ‌های حیاتی در برنامه‌های Android با شرط رقابت مرتبط هستند.

ویژگی کلیدی Race Condition غیرقطعی بودن آن است. همان کد می‌تواند هزاران بار بدون خطا کار کند و سپس ناگهان از کار بیفتد. این امر تشخیص را به ویژه دشوار می‌کند: باگ فقط در شرایط خاصی — بار CPU، تعداد نخ‌های فعال و فاز زمان‌بندی — ظاهر می‌شود.

شرط رقابت چگونه ایجاد می‌شود

عملیات غیراتمی

Race Condition زمانی ایجاد می‌شود که یک نخ عملیات غیراتمی — دنباله‌ای از چند مرحله که می‌تواند توسط نخ دیگری قطع شود — انجام می‌دهد. به عنوان مثال، عملیات افزایش counter++ در واقع از سه مرحله تشکیل شده است: خواندن مقدار از حافظه، افزایش یک واحد و نوشتن بازگشتی. اگر دو نخ این مراحل را به صورت درهم انجام دهند، نتیجه نادرست خواهد بود.

عدم همگام‌سازی

علت اصلی شرط رقابت — عدم همگام‌سازی در دسترسی به داده‌های مشترک. هنگامی که یک نخ شیء را تغییر می‌دهد و دیگری همزمان آن را می‌خواند، نتیجه خواندن غیرقابل پیش‌بینی است. در Android این مشکل بدتر می‌شود زیرا اجزای برنامه (Activity, Service, BroadcastReceiver) می‌توانند در نخ‌های مختلف اجرا شوند.

استفاده نادرست از کوروتین‌ها

در توسعه مدرن Android با Kotlin، Race Condition اغلب با استفاده نادرست از کوروتین‌ها ایجاد می‌شود. اگر دو کوروتین با وضعیت مشترک در Dispatchers مختلف بدون همگام‌سازی کار کنند، نتیجه غیرقابل پیش‌بینی خواهد بود. این امر به ویژه هنگام ترکیب Dispatchers.IO و Dispatchers.Main با اشیاء mutable مشترک رایج است.

مثال Race Condition در کد Kotlin

بیایید یک مثال کلاسیک از رقابت داده را بررسی کنیم — افزایش شمارنده از چندین نخ. بدون همگام‌سازی، مقدار نهایی کمتر از حد انتظار خواهد بود زیرا عملیات روی هم قرار می‌گیرند.

kotlin
class RaceCounter {
    private var counter = 0

    fun increment() {
        // عملیات غیراتمی — سه مرحله
        counter++  // می‌خواند، افزایش می‌دهد، می‌نویسد
    }

    fun getCount(): Int = counter
}

fun main() = runBlocking {
    val rc = RaceCounter()
    val jobs = List(1000) {
        launch(Dispatchers.Default) {
            rc.increment()
        }
    }
    jobs.forEach { it.join() }
    println(rc.getCount())  // انتظار 1000، دریافت ~997
}

در این مثال 1000 کوروتین به طور همزمان increment() را فراخوانی می‌کنند. به دلیل غیراتمی بودن عملیات counter++، مقدار نهایی تقریباً هرگز برابر 1000 نیست. هر بار اجرا نتیجه متفاوتی می‌دهد — علامت کلاسیک Race Condition. هرچه نخ‌های بیشتری در رقابت شرکت کنند، انحراف از مقدار مورد انتظار بیشتر است.

راه‌حل — استفاده از نوع اتمی یا قفل. در Kotlin برای این کار AtomicInteger از بسته java.util.concurrent.atomic مناسب است. این تضمین می‌کند که عملیات خواندن-تغییر-نوشتن به عنوان یک اقدام تقسیم‌ناپذیر واحد در سطح پردازنده انجام می‌شود.

kotlin
import java.util.concurrent.atomic.AtomicInteger

class SafeCounter {
    private val counter = AtomicInteger(0)

    fun increment() {
        counter.incrementAndGet()  // عملیات اتمی
    }

    fun getCount(): Int = counter.get()
}

انواع شرط‌های رقابت

رقابت داده (Data Race)

رقابت داده — رایج‌ترین نوع Race Condition. زمانی ایجاد می‌شود که یک نخ داده‌ها را در یک متغیر می‌نویسد و دیگری همزمان همان متغیر را بدون همگام‌سازی می‌خواند یا می‌نویسد. در Java Memory Model چنین رفتاری نامشخص تلقی می‌شود — نخ ممکن است به دلیل کش در سطح CPU مقدار غیربه‌روز را ببیند.

Check-Then-Act

الگوی Check-Then-Act — وضعیتی که نخ یک شرط را بررسی می‌کند و سپس بر اساس آن بررسی عملی انجام می‌دهد. بین بررسی و اقدام، نخ دیگری می‌تواند وضعیت را تغییر دهد. مثال معمول: بررسی وجود عنصر در مجموعه و سپس حذف آن. در Android این اغلب هنگام کار با SharedPreferences یا پایگاه داده رخ می‌دهد.

Read-Modify-Write

Read-Modify-Write — وضعیتی که نخ مقداری را می‌خواند، آن را در حافظه محلی تغییر می‌دهد و بازنویسی می‌کند. اگر بین خواندن و نوشتن نخ دیگری مقدار اصلی را تغییر داده باشد، نتیجه تغییر از دست می‌رود. مثال کلاسیک — عملیات counter++ که در کد Kotlin در بالا توضیح داده شد.

حافظه تراکنشی (STM)

Software Transactional Memory (STM) — رویکردی که در آن عملیات روی داده‌های مشترک در تراکنش‌ها مشابه پایگاه‌های داده انجام می‌شود. اگر دو تراکنش با هم تضاد داشته باشند، یکی بازگردانده و تکرار می‌شود. در Kotlin برای JVM کتابخانه Multiverse STM موجود است که به طور خودکار تضادهای دسترسی را بدون قفل‌های صریح مدیریت می‌کند. STM به ویژه در Android هنگام کار با چندین شیء به هم مرتبط مفید است.

رقابت‌های ریز در Android UI

دسته خاصی از Race Condition — رقابت‌های ریز (thin races) مرتبط با چرخه حیات Activity. سناریوی معمول: نخ پس‌زمینه بارگذاری داده را تمام می‌کند، اما Activity قبلاً نابود شده است (چرخش صفحه). کوروتین سعی می‌کند View ناموجود را به‌روزرسانی کند و با IllegalStateException از کار می‌افتد. راه‌حل — استفاده از viewModelScope و کامپوننت‌های Lifecycle-aware که هنگام نابودی Lifecycle Owner به طور خودکار کوروتین‌ها را لغو می‌کنند.

چگونه Race Condition را تشخیص دهیم

تشخیص Race Condition یکی از دشوارترین وظایف در اشکال‌زدایی برنامه‌های چندنخی است. آزمایش استاندارد به ندرت شرط رقابت را آشکار می‌کند زیرا فقط در شرایط خاص زمانی ظاهر می‌شود. به گفته Google (Android Testing Guide, 2023)، حدود 70% از Race Condition‌ها به دلیل ترتیب قطعی اجرا در محیط تست توسط تست‌های واحد تشخیص داده نمی‌شوند.

روش‌های اصلی تشخیص شامل ابزارهای تخصصی است. ThreadSanitizer (TSan) — تحلیلگر پویای تعبیه‌شده در Android NDK که تمام دسترسی‌های حافظه را ردیابی کرده و دسترسی‌های ناهمگام را شناسایی می‌کند. برای کد Java/Kotlin، Google Android Studio Layout Inspector را همراه با StrictMode توصیه می‌کند که دسترسی‌های غیرمجاز به نخ UI از نخ‌های پس‌زمینه را رهگیری می‌کند.

رویکرد مؤثر دیگر — Stress Testing با اجرای مکرر تست‌ها تحت بار. فریم‌ورک Lincheck از JetBrains به طور خاص برای تست ساختارهای داده همزمان روی JVM طراحی شده است. این فریم‌ورک به طور خودکار سناریوهایی با جایگشت‌های مختلف عملیات تولید کرده و صحت نتایج را در هر مورد بررسی می‌کند.

ابزارپلتفرمنوع تحلیل
ThreadSanitizerAndroid NDKتحلیل پویای حافظه
Intel InspectorWindowsایستا + پویا
LincheckJVM / Kotlinتست فشار
StrictModeAndroidرهگیری زمان اجرا

روش‌های پیشگیری از Race Condition

متغیرهای اتمی

متغیرهای اتمی (AtomicInteger, AtomicLong, AtomicReference) — ساده‌ترین راه برای حذف رقابت داده برای عملیات تکی. آنها از دستورالعمل‌های سطح پایین CAS پردازنده (Compare-And-Swap) استفاده می‌کنند که بدون قفل به صورت اتمی اجرا می‌شوند. این حداکثر عملکرد را در سناریوهای با رقابت کم فراهم می‌کند.

قفل‌ها و Mutex

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

جداسازی حالت

جداسازی حالت — رویکرد معماری که در آن هر نخ با کپی مخصوص خود از داده‌ها کار می‌کند. در توسعه موبایل این امر از طریق مدل Actor به دست می‌آید، جایی که هر actor مالک حالت خود است و با actorهای دیگر پیام رد و بدل می‌کند. Kotlin Coroutines پیاده‌سازی Actor را از طریق Channel و SendChannel فراهم می‌کند که Race Condition را در سطح معماری کاملاً حذف می‌کند.

سطح حفاظتی اضافی — Immutability: اگر داده‌های مشترک ذاتاً تغییرناپذیر باشند، Race Condition حتی بدون همگام‌سازی غیرممکن می‌شود. در Kotlin برای این کار از data class با فیلدهای val و مجموعه‌های kotlinx.collections.immutable استفاده می‌شود که تغییرناپذیری ساختار را هنگام انتشار بین نخ‌ها تضمین می‌کنند.

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

تفاوت بین Race Condition و Data Race چیست؟

Data Race — نوع خاصی از Race Condition است که در آن دو نخ به طور همزمان به یک حافظه دسترسی پیدا می‌کنند و حداقل یکی از آنها نوشتن انجام می‌دهد. Race Condition مفهوم گسترده‌تری است که هر خطای وابسته به ترتیب اجرای نخ‌ها، از جمله شرط‌های رقابت منطقی را شامل می‌شود.

آیا می‌توان Race Condition را در Android کاملاً حذف کرد؟

حذف کامل غیرممکن است، اما می‌توان آن را به حداقل رساند. از اشیاء تغییرناپذیر (immutable)، انواع اتمی و کوروتین‌ها با توزیع‌کننده تک‌نخی استفاده کنید. ابزارهای تحلیل ایستا مانند Android Lint با قانون ThreadSafety به شناسایی رقابت‌های بالقوه در مرحله کامپایل کمک می‌کنند.

Race Condition چگونه در برنامه‌های UI ظاهر می‌شود؟

در برنامه‌های UI Race Condition اغلب به صورت سوسو زدن صفحه، نمایش نادرست داده‌ها یا خرابی هنگام به‌روزرسانی لیست ظاهر می‌شود. سناریوی معمول: نخ پس‌زمینه داده‌ها را بارگیری کرده و آداپتور را به‌روز می‌کند، در حالی که کاربر در همین لحظه لیست را پیمایش می‌کند — دسترسی همزمان به Adapter DataSet ایجاد می‌شود.

volatile چیست و آیا به Race Condition کمک می‌کند؟

volatile قابلیت مشاهده تغییرات بین نخ‌ها را تضمین می‌کند — نوشتن در متغیر volatile بلافاصله برای همه نخ‌ها قابل مشاهده است. با این حال volatile مشکل Read-Modify-Write و Check-Then-Act را حل نمی‌کند، زیرا اتمی بودن عملیات مرکب را تضمین نمی‌کند. برای چنین سناریوهایی قفل‌ها یا کلاس‌های اتمی مورد نیاز است.

Race Condition در Kotlin Coroutines چه تفاوتی با نخ‌های کلاسیک دارد؟

در Kotlin Coroutines، Race Condition در سطح زمان‌بند کوروتین ایجاد می‌شود، نه زمان‌بند نخ سیستم عامل. کوروتین‌ها می‌توانند در نقاط تعلیق (suspend) سوئیچ شوند که فرصت‌های اضافی برای رقابت ایجاد می‌کند. ابزار kotlinx.coroutines.debug و دیباگر IntelliJ IDEA به ردیابی وضعیت کوروتین‌ها کمک می‌کنند.

خلاصه

  • Race Condition — خطای کد چندنخی که نتیجه به ترتیب غیرقابل پیش‌بینی اجرای نخ‌ها بستگی دارد
  • Data Race — زیرمجموعه شرط رقابت که هنگام دسترسی همزمان ناهمگام به حافظه با نوشتن ایجاد می‌شود
  • عملیات غیراتمی (Read-Modify-Write, Check-Then-Act) — علت اصلی ایجاد رقابت نخ‌ها
  • ThreadSanitizer و Lincheck — ابزارهای مؤثر برای تشخیص Race Condition در مرحله تست
  • متغیرهای اتمی (AtomicInteger) — راه بهینه برای محافظت از عملیات تکی بدون قفل
  • Mutex و مدل Actor — رویکردهای معماری برای محافظت از بخش‌های بحرانی پیچیده
  • جداسازی حالت از طریق اشیاء تغییرناپذیر و توزیع‌کننده‌های تک‌نخی Race Condition را در سطح طراحی کاملاً حذف می‌کند

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

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

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

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