Heisenbug: چیست، چرا رخ می‌دهد و روش‌های ردیابی

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

Heisenbug — باگی که هنگام تلاش برای دیباگ ناپدید می‌شود. این اصطلاح از اصل عدم قطعیت هایزنبرگ گرفته شده است: مشاهده بر رفتار سیستم تأثیر می‌گذارد. در توسعه موبایل، Heisenbug یکی از دشوارترین مشکلات است زیرا روش‌های استاندارد دیباگ (لاگ‌ها، نقاط شکست، کد اضافی) وضعیت برنامه را تغییر می‌دهند و باگ را پنهان می‌کنند. علل بروز و روش‌های مقابله با خطاهای گریزان را بررسی می‌کنیم.

نکات اصلی

  • Race condition — علت اصلی Heisenbug: تغییر زمان‌بندی هنگام دیباگ مشکل را پنهان می‌کند
  • Bohrbug — باگ قابل پیش‌بینی، برخلاف Heisenbug به راحتی تکرارپذیر است
  • Mandelbug — باگ با رابطه علت و معلولی پیچیده، حساس به شرایط اولیه
  • ThreadSanitizer — ابزاری برای تشخیص رقابت داده‌ها بدون تأثیر بر زمان‌بندی
  • تست‌های قطعی — تنها راه مطمئن برای تکرار Heisenbug

Heisenbug در توسعه موبایل چیست؟

Heisenbug — دسته‌ای از خطاها که در محیط تولید یا هنگام کار عادی ظاهر می‌شوند، اما هنگام تلاش برای تکرار در محیط دیباگ ناپدید می‌شوند. این اصطلاح در دهه ۱۹۸۰ توسط برنامه‌نویس Jim Gray در زمینه سیستم‌های توزیع‌شده معرفی شد، اما امروز به دلیل ماهیت ناهمزمان برنامه‌های موبایل بسیار مرتبط است.

علت اصلی: ابزارهای استاندارد دیباگ محیط اجرا را تغییر می‌دهند. نقطه شکست (breakpoint) نخ را برای چند میلی‌ثانیه متوقف می‌کند، لاگ‌گیری I/O همزمان اضافه می‌کند، بررسی‌های اضافی ترتیب عملیات را تغییر می‌دهند. در محیط چندنخی، حتی تأخیر میکروثانیه‌ای می‌تواند ترتیب اجرای نخ‌ها را تغییر دهد و رقابت داده را پنهان کند.

بر اساس داده‌های Microsoft Research (۲۰۲۲)، حدود ۱۵-۲۵٪ از همه باگ‌ها در برنامه‌های موبایل چندنخی به عنوان Heisenbug طبقه‌بندی می‌شوند. زمان یافتن و رفع یک Heisenbug به طور متوسط ۵-۱۰ برابر بیشتر از یک باگ معمولی است، زیرا تکرار مستقیم امکان‌پذیر نیست.

مثال Heisenbug

برنامه در محیط تولید هنگام کشیدن سریع لیست کرش می‌کند، اما پس از اتصال دیباگر یا اضافه کردن لاگ‌ها عالی کار می‌کند. علت: رقابت داده بین نخ UI (به‌روزرسانی RecyclerView) و نخ پس‌زمینه (به‌روزرسانی داده‌های آداپتر). لاگ‌ها تأخیری اضافه می‌کنند که نخ‌ها را به طور تصادفی همگام‌سازی می‌کند.

Bohrbug، Mandelbug، Heisenbug: طبقه‌بندی باگ‌ها

Bohrbug — باگ قابل پیش‌بینی و پایدار قابل تکرار. این نام به قیاس با مدل اتمی بور گرفته شده است: مانند اتم، باگ در هر مشاهده یکسان رفتار می‌کند. مثال: NullPointerException هنگام کلیک روی دکمه قبل از بارگذاری داده. با تست واحد استاندارد درمان می‌شود.

Mandelbug — باگ با رابطه علت و معلولی پیچیده و آشوب‌گونه (به قیاس با مجموعه ماندلبرو نامگذاری شده است). فقط در ترکیب خاصی از شرایط ظاهر می‌شود: نسخه سیستم عامل، مدل دستگاه، وضعیت شبکه، فاز ماه. تفاوت آن با Heisenbug این است که در هنگام دیباگ ناپدید نمی‌شود — مشکل در دشواری تکرار است، نه تغییر رفتار توسط ابزارها.

Heisenbug — باگی که دقیقاً به دلیل ابزارهای دیباگ ناپدید می‌شود. اگر لاگ اضافه کنید — باگ ناپدید می‌شود. اگر نقطه شکست بگذارید — باگ ظاهر نمی‌شود. اگر همه چیز را بردارید — باگ برمی‌گردد. علت اصلی: زمان‌بندی تغییر یافته در هنگام دیباگ.

نوعتکرارپذیریواکنش به دیباگمثال
Bohrbug۱۰۰٪تغییر نمی‌کندNPE در لیست خالی
Mandelbugآشوب‌گونهتغییر نمی‌کندکرش در Android 12، سامسونگ، با باتری کم
Heisenbugفقط بدون دیباگناپدید می‌شودRace condition ناپدیدشونده با لاگ‌ها
Schrödinbugدر کد ظاهر نمی‌شودبا نگاه ظاهر می‌شودباگ قابل مشاهده در کد که هرگز رخ نمی‌دهد

علل اصلی بروز Heisenbug

Race condition — شماره یک در میان علل Heisenbug. دو نخ بدون همگام‌سازی به داده‌های مشترک دسترسی پیدا می‌کنند. دیباگر تأخیری ایجاد می‌کند که باعث می‌شود نخ‌ها به طور طبیعی همگام شوند. بدون دیباگر ترتیب اجرا غیرقابل پیش‌بینی است.

خطاهای وابسته به زمان‌بندی — خطاهایی که فقط در سرعت خاصی از اجرا ظاهر می‌شوند. مثلاً انیمیشنی که باید قبل از شروع عملیات بعدی کامل شود. در دیباگر انیمیشن کندتر اجرا می‌شود و عملیات بعد از اتمام انیمیشن شروع می‌شود. در تولید — برعکس.

kotlin
// مثال race condition — Heisenbug معمولی
class ListViewModel : ViewModel() {
    private var items = mutableListOf<String>()

    fun loadFromNetwork() {
        viewModelScope.launch(Dispatchers.IO) {
            val result = api.fetchItems()
            items.addAll(result) // ❌ ایمن نخی نیست
        }
    }

    fun getItems(): List<String> = items.toList()
    // Race condition: خواندن getItems ممکن است با نوشتن loadFromNetwork همپوشانی داشته باشد
}

بهینه‌سازی کامپایلر — کامپایلر (JIT، ART، Kotlin/Native) ممکن است برای بهینه‌سازی دستورالعمل‌ها را مرتب کند. در نسخه دیباگ (debug build) بهینه‌سازی‌ها غیرفعال هستند و کد «آنطور که نوشته شده» اجرا می‌شود. در release build کامپایلر ترتیب عملیات را تغییر می‌دهد که می‌تواند فرضیات پنهان در کد را آشکار کند.

  • ThreadLocal — استفاده نادرست از متغیرهای thread-local که برای سایر نخ‌ها قابل مشاهده نیستند
  • متغیرهای مقداردهی نشده — کدی که به مقادیر پیش‌فرض فیلدهای کلاس متکی است
  • صف‌های GCD/dispatch — در iOS ترتیب نامشخص اجرای بلوک‌ها در صف‌های concurrent
  • I/O بافر شده — داده‌ها تا پر شدن بافر روی دیسک نوشته نمی‌شوند

استراتژی‌های ردیابی باگ‌های گریزان

ThreadSanitizer (TSan) — ابزار Google برای تشخیص رقابت داده در C/C++ و Kotlin/Native. در build جاسازی می‌شود و هر دسترسی به حافظه مشترک بدون همگام‌سازی را تشخیص می‌دهد. بر خلاف لاگ‌ها، TSan بر زمان‌بندی تأثیر نمی‌گذارد زیرا از طریق instrumented code کار می‌کند، نه از طریق I/O.

تست‌های قطعی — ناهمزمانی واقعی را با کنترل‌شده جایگزین کنید. برای کنترل کامل روی ترتیب اجرا از TestDispatcher (Kotlin)، RxJava Plugins یا GCD test queues (iOS) استفاده کنید. سناریوهای خاص تعیین کنید: نخ A اجرا می‌شود، سپس B، سپس A دوباره.

لاگ‌گیری چرخه‌ای — لاگ‌گیری در بافر چرخه‌ای در حافظه (نه روی دیسک). وقتی باگ رخ می‌دهد، بافر در فایل ذخیره می‌شود. از آنجایی که نوشتن در حافظه نانوثانیه طول می‌کشد (به جای میلی‌ثانیه برای I/O دیسک)، چنین لاگی بر زمان‌بندی تأثیر نمی‌گذارد و Heisenbug را پنهان نمی‌کند.

kotlin
class CyclicBuffer(val capacity: Int = 1000) {
    private val buffer = ArrayDeque<String>(capacity)
    private val lock = Any()

    fun log(message: String) {
        synchronized(lock) {
            if (buffer.size >= capacity) buffer.removeFirst()
            buffer.addLast(message)
        }
    }

    fun flush() {
        synchronized(lock) { buffer.forEach { fileWriter.write(it) } }
    }
}

لاگ‌گیری در تولید — اگر باگ به صورت محلی تکرار نمی‌شود، در تولید داده جمع‌آوری کنید. از Firebase Crashlytics logs، Sentry Breadcrumbs یا لاگر چرخه‌ای سفارشی استفاده کنید. مهم: لاگ‌گیری باید ناهمزمان باشد و حداقل تأثیر را بر عملکرد داشته باشد.

پیشگیری از Heisenbug در سطح معماری

ایزوله کردن وضعیت — state اشتراکی قابل تغییر را به حداقل برسانید. هر مؤلفه باید وضعیت ایزوله خود را داشته باشد که برای نوشتن مستقیم از سایر مؤلفه‌ها قابل دسترسی نباشد. از Unidirectional Data Flow (UDF) استفاده کنید — وضعیت در یک جهت جریان دارد: Event → Reducer → State → UI.

رویکرد تابعی — توابع خالص بدون عوارض جانبی تست و دیباگ آسان‌تری دارند. عوارض جانبی (شبکه، پایگاه داده، فایل‌ها) را در لایه‌های کاملاً مشخص (repository، data source) ایزوله کنید. خطاهای نخی در کد تابعی عملاً غیرممکن هستند.

حالت سختگیرانه — Android StrictMode را در debug build فعال کنید. نقض سیاست نخ‌بندی (شبکه در نخ اصلی، I/O دیسک در نخ اصلی) را تشخیص می‌دهد و استثنا پرتاب می‌کند. این کار Heisenbug بالقوه را به Bohrbug قطعی و بلافاصله قابل مشاهده تبدیل می‌کند.

kotlin
class DebugApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()
                    .penaltyLog()
                    .build()
            )
        }
    }
}

بازبینی کد با تمرکز بر ناهمزمانی — بخش اجباری فرآیند. هر pull request باید از نظر state اشتراکی قابل تغییر، مجموعه‌های ناایمن نخی، عدم همگام‌سازی بررسی شود. برای ممنوعیت خودکار الگوهای خاص از قوانین lint استفاده کنید (مثلاً دسترسی به MutableList بدون synchronized).

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

چرا Heisenbug اینقدر سخت پیدا می‌شود؟

زیرا روش‌های استاندارد — نقاط شکست، لاگ‌ها، print — محیط اجرا را آنقدر تغییر می‌دهند که باگ دیگر ظاهر نمی‌شود. دیباگر همه نخ‌ها را برای ده‌ها میلی‌ثانیه متوقف می‌کند. در این مدت race condition که باعث باگ می‌شد به طور طبیعی حل می‌شود. به ابزارهایی نیاز است که بر زمان‌بندی اجرا تأثیر نگذارند.

تفاوت Heisenbug با Mandelbug چیست؟

Mandelbug به دلیل پیچیدگی شرایط به سختی تکرار می‌شود، اما ابزارهای دیباگ بر ظاهر آن تأثیر نمی‌گذارند. Heisenbug دقیقاً از ابزارهای دیباگ ناپدید می‌شود. مثال Mandelbug: کرش فقط در دستگاه‌های Android 11 با ۳ گیگابایت رم و سطح باتری زیر ۱۵٪. مثال Heisenbug: race condition که با اضافه کردن Log.d() ناپدید می‌شود.

چگونه Heisenbug را در CI/CD تست کنیم؟

Flaky test detection را اجرا کنید — تست‌هایی که گاهی رد می‌شوند، گاهی قبول. در Android از Android Test Orchestrator برای ایزوله کردن تست‌ها استفاده کنید. StrictMode را به تست‌های debug اضافه کنید. build را با ThreadSanitizer ابزارگذاری کنید. اگر تست در بیش از ۵٪ اجراها flaky است — آن را Heisenbug بالقوه در نظر بگیرید و قبل از merge بررسی کنید.

آیا Flow/Coroutines به جلوگیری از Heisenbug کمک می‌کنند؟

تا حدی. Flow و structured concurrency در Kotlin مقدار state اشتراکی قابل تغییر را کاهش می‌دهند و مدیریت نخ‌ها را ساده می‌کنند. اما Coroutines ایمنی نخی را تضمین نمی‌کنند: اگر دو coroutine state اشتراکی داشته باشند، race condition همچنان ممکن است. برای محافظت از state مشترک از Mutex یا برای انتقال داده بین coroutine‌ها از Channel استفاده کنید.

اگر Heisenbug فقط در تولید ظاهر شود چه باید کرد؟

از بافر چرخه‌ای لاگ در حافظه با تخلیه خودکار هنگام خطا استفاده کنید. نظارت دقیق از طریق Crashlytics یا Sentry با breadcrumbs سفارشی اضافه کنید. برای Android ANR detection را فعال کنید و traceها را بررسی کنید. اگر باگ race condition است، ThreadSanitizer در debug build با بار نزدیک به تولید می‌تواند مشکل را آشکار کند.

خلاصه

  • Heisenbug — باگی که هنگام تلاش برای دیباگ ناپدید می‌شود؛ علت اصلی — تغییر زمان‌بندی توسط ابزارهای توسعه‌دهنده
  • Race condition — علت اصلی Heisenbug در برنامه‌های موبایل، به ویژه در کد ناهمزمان
  • Bohrbug (۱۰۰٪ تکرارپذیر) و Mandelbug (آشوب‌گونه) — انواع دیگر باگ، با Heisenbug اشتباه گرفته نشوند
  • ThreadSanitizer — بهترین ابزار برای تشخیص رقابت داده بدون تأثیر بر زمان‌بندی اجرا
  • لاگ‌گیری چرخه‌ای در حافظه به جای دیسک — روش جمع‌آوری داده بدون پنهان کردن Heisenbug
  • Unidirectional Data Flow و به حداقل رساندن state اشتراکی قابل تغییر — پیشگیری معماری از یک دسته کامل از خطاها
  • StrictMode در debug build Heisenbug بالقوه را به Bohrbug قطعی و بلافاصله قابل مشاهده تبدیل می‌کند

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

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

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

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