Heisenbug: ما هو، لماذا يحدث وطرق اكتشافه

المؤلف: IT Sectr نُشر: 2026-07-29 وقت القراءة: 10 دق

Heisenbug — خطأ يختفي عند محاولة تصحيحه. المصطلح مشتق من مبدأ عدم اليقين لهايزنبرغ: الملاحظة تؤثر على سلوك النظام. في تطوير التطبيقات، Heisenbug هو واحد من أصعب المشاكل لأن طرق التصحيح القياسية (السجلات، نقاط التوقف، الكود الإضافي) تغير حالة البرنامج وتخفي الخطأ. سنستعرض الأسباب وطرق مكافحة الأخطاء المراوغة.

الخلاصة

  • Race condition — السبب الرئيسي لـ Heisenbug: تغيير التوقيت أثناء التصحيح يخفي المشكلة
  • Bohrbug — خطأ يمكن التنبؤ به، يسهل إعادة إنتاجه على عكس Heisenbug
  • Mandelbug — خطأ بعلاقات سببية معقدة، حساس للظروف الأولية
  • ThreadSanitizer — أداة لاكتشاف تسابق البيانات دون التأثير على التوقيت
  • الاختبارات الحتمية — الطريقة الوحيدة الموثوقة لإعادة إنتاج Heisenbug

ما هو Heisenbug في تطوير التطبيقات؟

Heisenbug — فئة من الأخطاء التي تظهر في بيئة الإنتاج أو أثناء التشغيل الطبيعي، لكنها تختفي عند محاولة إعادة إنتاجها في بيئة التصحيح. صاغ المصطلح في الثمانينيات المبرمج Jim Gray في سياق الأنظمة الموزعة، لكنه الأكثر صلة اليوم بتطبيقات الجوال بسبب طبيعتها غير المتزامنة.

السبب الرئيسي: أدوات التصحيح القياسية تغير بيئة التنفيذ. نقطة التوقف توقف الخيط لبضع ملي ثوان، التسجيل يضيف إدخال/إخراج متزامن، الفحوصات الإضافية تغير ترتيب العمليات. في بيئة متعددة الخيوط، حتى تأخير ميكروثانية يمكن أن يغير ترتيب تنفيذ الخيوط ويخفي تسابق البيانات.

وفقاً لـ Microsoft Research (2022)، حوالي 15-25% من جميع الأخطاء في تطبيقات الجوال متعددة الخيوط تصنف كـ Heisenbug. في نفس الوقت، وقت البحث وإصلاح Heisenbug واحد أطول بمعدل 5-10 مرات من الخطأ العادي، بسبب استحالة إعادة إنتاجه مباشرة.

مثال على Heisenbug

تتعطل التطبيقات في الإنتاج عند التمرير السريع عبر القائمة، ولكن عند الاتصال بالمصحح أو إضافة السجلات — تعمل بشكل مثالي. السبب: تسابق البيانات بين خيط الواجهة (تحديث RecyclerView) وخيط الخلفية (تحديث بيانات المحول). السجلات تضيف تأخيراً يزامن الخيوط بشكل عشوائي.

Bohrbug، Mandelbug، Heisenbug: تصنيف الأخطاء

Bohrbug — خطأ يمكن التنبؤ به، يمكن إعادة إنتاجه بثبات. سمي تشبيهاً بنموذج بور الذري: مثل الذرة، يتصرف الخطأ بنفس الطريقة في كل مرة يُلاحظ فيها. مثال: NullPointerException عند النقر على زر قبل تحميل البيانات. يُعالج بالاختبارات الوحدوية القياسية.

Mandelbug — خطأ بعلاقة سببية معقدة وفوضوية (سمي تشبيهاً بمجموعة ماندلبروت). يظهر فقط تحت مجموعة معينة من الظروف: إصدار نظام التشغيل، طراز الجهاز، حالة الشبكة. يختلف عن Heisenbug في أنه لا يختفي أثناء التصحيح — المشكلة هي صعوبة إعادة الإنتاج، وليس تغير السلوك من الأدوات.

Heisenbug — خطأ يختفي تحديداً بسبب أدوات التصحيح. إذا أضفت سجلاً — يختفي الخطأ. إذا وضعت نقطة توقف — لا يظهر الخطأ. إذا أزلت كل شيء — يعود الخطأ. السبب الرئيسي: تغير التوقيت أثناء التصحيح.

النوعقابلية إعادة الإنتاجرد الفعل على التصحيحمثال
Bohrbug100%لا يتغيرNPE عند قائمة فارغة
Mandelbugفوضويةلا يتغيرتعطل على Android 12، Samsung، بطارية منخفضة
Heisenbugفقط بدون تصحيحيختفيتسابق بيانات يختفي مع السجلات
Schrödinbugلا يظهر في الكوديظهر عند النظرخطأ مرئي في الكود لكنه لا يحدث أبداً

الأسباب الرئيسية لظهور Heisenbug

Race condition — السبب الأول لـ Heisenbug. خيطان يصلان إلى بيانات مشتركة دون مزامنة. المصحح يقدم تأخيراً، مما يؤدي إلى مزامنة الخيوط بشكل طبيعي. بدون المصحح، ترتيب التنفيذ غير متوقع.

الأخطاء المعتمدة على التوقيت — أخطاء تظهر فقط عند سرعة تنفيذ معينة. على سبيل المثال، رسم متحرك يجب أن ينتهي قبل بدء العملية التالية. في المصحح، الرسم المتحرك أبطأ، وتكون العملية قد بدأت بعد انتهاء الرسم. في الإنتاج — العكس.

kotlin
// Example race condition — typical Heisenbug
class ListViewModel : ViewModel() {
    private var items = mutableListOf<String>()

    fun loadFromNetwork() {
        viewModelScope.launch(Dispatchers.IO) {
            val result = api.fetchItems()
            items.addAll(result) // ❌ Not thread-safe
        }
    }

    fun getItems(): List<String> = items.toList()
    // Race condition: getItems read may overlap with loadFromNetwork write
}

تحسين المترجم — يمكن للمترجم (JIT، ART، Kotlin/Native) إعادة ترتيب التعليمات للتحسين. في بناء التصحيح (debug build)، التعطيلات معطلة والكود ينفذ «كما هو مكتوب». في بناء الإصدار (release build)، يغير المترجم ترتيب العمليات، مما قد يكشف افتراضات مخفية في الكود.

  • ThreadLocal — استخدام غير صحيح لمتغيرات thread-local غير المرئية للخيوط الأخرى
  • متغيرات غير مهيأة — كود يعتمد على القيم الافتراضية لحقول الفئة
  • قوائم GCD/dispatch — في iOS، ترتيب غير محدد لتنفيذ الكتل في قوائم متزامنة
  • إدخال/إخراج مخزن — البيانات لا تُكتب على القرص حتى يمتلئ المخزن المؤقت

استراتيجيات اكتشاف الأخطاء المراوغة

ThreadSanitizer (TSan) — أداة جوجل لاكتشاف تسابق البيانات في C/C++ و Kotlin/Native. تُضمن في البناء وتكشف أي وصول للذاكرة المشتركة دون مزامنة. على عكس السجلات، TSan لا يؤثر على التوقيت لأنه يعمل من خلال كود مُجهز، وليس من خلال الإدخال/الإخراج.

الاختبارات الحتمية — استبدل عدم التزامن الحقيقي بعدم تزامن خاضع للتحكم. استخدم TestDispatcher (Kotlin)، RxJava Plugins أو قوائم اختبار GCD (iOS) للتحكم الكامل في ترتيب التنفيذ. حدد سيناريوهات محددة: الخيط A ينفذ، ثم B، ثم A مرة أخرى.

التسجيل الدوري — التسجيل في مخزن مؤقت حلقي في الذاكرة (وليس على القرص). عندما يحدث الخطأ، يُحفظ المخزن المؤقت في ملف. نظراً لأن الكتابة في الذاكرة تستغرق نانو ثانية (بدلاً من ملي ثانية لإدخال/إخراج القرص)، فإن هذا التسجيل لا يؤثر على التوقيت ولا يخفي 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، Sentry Breadcrumbs أو مسجل دوري مخصص. مهم: يجب أن يكون التسجيل غير متزامن وذو تأثير ضئيل على الأداء.

الوقاية من Heisenbug على مستوى البنية

عزل الحالة — قلل من الحالة القابلة للتغيير المشتركة. يجب أن يكون لكل مكون حالته المعزولة الخاصة، غير القابلة للكتابة المباشرة من المكونات الأخرى. استخدم تدفق البيانات أحادي الاتجاه (UDF) — تتدفق الحالة في اتجاه واحد: حدث → مختزل → حالة → واجهة.

النهج الوظيفي — الدوال الخالصة دون آثار جانبية أسهل في الاختبار والتصحيح. اعزل الآثار الجانبية (الشبكة، قاعدة البيانات، الملفات) في طبقات محددة بدقة (مستودع، مصدر بيانات). أخطاء الخيوط في الكود الوظيفي شبه مستحيلة.

الوضع الصارم — فعّل Android StrictMode في بناء التصحيح. يكتشف انتهاكات سياسة الخيوط (الشبكة على الخيط الرئيسي، إدخال/إخراج القرص على الخيط الرئيسي) ويرمي استثناء. هذا يحول Heisenbug محتمل إلى Bohrbug حتمي مرئي فوراً.

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

مراجعة الكود مع التركيز على عدم التزامن — جزء إلزامي من العملية. يجب فحص كل طلب سحب بحثاً عن حالة قابلة للتغيير مشتركة، مجموعات غير آمنة للخيوط، وغياب المزامنة. استخدم قواعد lint لمنع أنماط معينة تلقائياً (مثل الوصول إلى MutableList بدون synchronized).

الأسئلة الشائعة

لماذا يصعب العثور على Heisenbug؟

لأن الطرق القياسية — نقاط التوقف، السجلات، print — تغير بيئة التنفيذ لدرجة أن الخطأ يتوقف عن الظهور. المصحح يوقف جميع الخيوط لعشرات الملي ثوان. خلال هذا الوقت، يتلاشى تسابق البيانات الذي تسبب في الخطأ بشكل طبيعي. نحتاج أدوات لا تؤثر على توقيت التنفيذ.

كيف يختلف Heisenbug عن Mandelbug؟

Mandelbug يصعب إعادة إنتاجه بسبب تعقيد الظروف، لكن أدوات التصحيح لا تؤثر على ظهوره. Heisenbug يختفي تحديداً بسبب أدوات التصحيح. مثال Mandelbug: تعطل فقط على أجهزة Android 11، 3 GB RAM، ومستوى بطارية أقل من 15%. مثال Heisenbug: تسابق بيانات يختفي عند إضافة Log.d().

كيف نختبر Heisenbug في CI/CD؟

استخدم كشف الاختبارات غير المستقرة — اختبارات تارة تفشل وتارة تنجح. في Android، استخدم Android Test Orchestrator لعزل الاختبارات. أضف StrictMode لاختبارات التصحيح. جهز البناء بـ ThreadSanitizer. إذا كان الاختبار غير مستقر >5% من التشغيلات — اعتبره Heisenbug محتملاً وافحصه قبل الدمج.

هل يساعد Flow/Coroutines في تجنب Heisenbug؟

جزئياً. Flow و التزامن المنظم في Kotlin يقللان من كمية الحالة القابلة للتغيير المشتركة ويبسطان إدارة الخيوط. لكن coroutines لا تضمن أمان الخيوط: إذا شاركت اثنتان من coroutines الحالة، فلا يزال تسابق البيانات ممكناً. استخدم Mutex لحماية الحالة المشتركة أو Channel لنقل البيانات بين coroutines.

ماذا تفعل إذا ظهر Heisenbug فقط في الإنتاج؟

استخدم مخزناً دورياً للسجلات في الذاكرة مع تفريغ تلقائي عند الخطأ. أضف مراقبة مفصلة عبر Crashlytics أو Sentry مع breadcrumbs مخصصة. بالنسبة لـ Android، فعّل كشف ANR وراجع التتبعات. إذا كان الخطأ تسابق بيانات، فإن ThreadSanitizer في بناء تصحيح مع حمل مشابه للإنتاج قد يكشف المشكلة.

الملخص

  • Heisenbug — خطأ يختفي عند محاولة تصحيحه؛ السبب الرئيسي هو تغيرات التوقيت من أدوات المطور
  • Race condition — السبب الرئيسي لـ Heisenbug في تطبيقات الجوال، خاصة في الكود غير المتزامن
  • Bohrbug (قابلية إعادة إنتاج 100%) و Mandelbug (فوضوي) — أنواع أخرى من الأخطاء، لا تخلط مع Heisenbug
  • ThreadSanitizer — أفضل أداة لاكتشاف تسابق البيانات، دون التأثير على توقيت التنفيذ
  • التسجيل الدوري في الذاكرة بدلاً من القرص — طريقة لجمع البيانات دون إخفاء Heisenbug
  • تدفق البيانات أحادي الاتجاه وتقليل الحالة القابلة للتغيير المشتركة — وقاية معمارية لفئة كاملة من الأخطاء
  • StrictMode في بناء التصحيح يحول Heisenbug محتملاً إلى Bohrbug حتمي مرئي فوراً

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا