Race Condition — ملٹی تھریڈڈ پروگرامنگ میں وہ صورت حال ہے جب حتمی نتیجہ اس بات پر منحصر ہوتا ہے کہ تھریڈز کس ترتیب میں عمل میں آتے ہیں۔ دستاویزات Oracle Java Tutorials (2024) کے مطابق، ریس کنڈیشن بغیر ہم آہنگی کے مشترکہ وسائل تک بیک وقت رسائی سے پیدا ہوتی ہے۔ مناسب میکانزم کے بغیر Race Condition ڈیٹا کو نقصان اور موبائل ایپلیکیشنز میں ناقابل تولید بگز کی طرف لے جاتا ہے۔
اہم نکات
Race Condition (ریس کنڈیشن) — ملٹی تھریڈڈ پروگرام میں ایک خرابی ہے جہاں کام کی درستگی تھریڈز کے ناقابل پیشگوئی عمل درآمد ترتیب پر منحصر ہوتی ہے۔ جب دو یا زیادہ تھریڈز بغیر ہم آہنگی کے بیک وقت مشترکہ وسائل تک رسائی کرتے ہیں، تو وسائل کی حتمی حالت غیر متعین ہو جاتی ہے۔
موبائل ڈیولپمنٹ میں، Race Condition خاص طور پر خطرناک ہے کیونکہ تھریڈز مختلف رفتاروں پر پروسیسر کے مختلف کوروں پر عمل میں آ سکتے ہیں۔ ڈیولپر کنٹرول نہیں کر سکتا کہ کون سا تھریڈ پہلے کارروائی مکمل کرے گا — یہ آپریٹنگ سسٹم کا شیڈیولر طے کرتا ہے۔ IBM (Concurrency Bugs in Android, 2022) کی تحقیق کے مطابق، Android ایپلیکیشنز میں تقریباً 23% سنگین بگز ریس کنڈیشن سے متعلق ہوتے ہیں۔
Race Condition کی اہم خصوصیت اس کی غیر تعینیتی ہے۔ وہی کوڈ ہزار بار بغیر خرابی کے کام کر سکتا ہے اور پھر اچانک کریش ہو سکتا ہے۔ یہ تشخیص کو خاص طور پر مشکل بناتا ہے: بگ صرف مخصوص حالات میں ظاہر ہوتا ہے — CPU لوڈ، فعال تھریڈز کی تعداد اور شیڈیولنگ مرحلے پر منحصر۔
Race Condition اس وقت پیدا ہوتی ہے جب تھریڈ غیر ایٹمی کارروائی انجام دیتا ہے — کئی مراحل کا ایک سلسلہ جو دوسرے تھریڈ کے ذریعے روکا جا سکتا ہے۔ مثال کے طور پر، counter++ اضافے کی کارروائی دراصل تین مراحل پر مشتمل ہے: میموری سے قدر پڑھنا، ایک سے بڑھانا اور واپس لکھنا۔ اگر دو تھریڈز ان مراحل کو ملا کر انجام دیں، تو نتیجہ غلط ہو گا۔
ریس کنڈیشن کی بنیادی وجہ مشترکہ ڈیٹا تک رسائی کے وقت ہم آہنگی کی عدم موجودگی ہے۔ جب ایک تھریڈ آبجیکٹ کو تبدیل کرتا ہے اور دوسرا بیک وقت اسے پڑھتا ہے، تو پڑھائی کا نتیجہ غیر متوقع ہوتا ہے۔ Android میں، یہ مسئلہ اس حقیقت سے بڑھ جاتا ہے کہ ایپلیکیشن کے اجزاء (Activity, Service, BroadcastReceiver) مختلف تھریڈز میں عمل میں آ سکتے ہیں۔
Kotlin میں جدید Android ڈیولپمنٹ میں، Race Condition اکثر کوروٹین کے غلط استعمال سے پیدا ہوتی ہے۔ اگر دو کوروٹین بغیر ہم آہنگی کے مختلف Dispatchers میں مشترکہ حالت کے ساتھ کام کرتی ہیں، تو نتیجہ غیر متوقع ہو گا۔ یہ خاص طور پر مشترکہ mutable-آبجیکٹ کے ساتھ Dispatchers.IO اور Dispatchers.Main کے امتزاج پر ہوتا ہے۔
ڈیٹا ریس کی ایک کلاسک مثال دیکھتے ہیں — متعدد تھریڈز سے کاؤنٹر اضافہ۔ ہم آہنگی کے بغیر، حتمی قدر توقع سے کم ہوگی کیونکہ کارروائیاں ایک دوسرے پر اوورلیپ ہوتی ہیں۔
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 میں اس کام کے لیے java.util.concurrent.atomic پیکیج سے AtomicInteger موزوں ہے۔ یہ یقینی بناتا ہے کہ پڑھائی-ترمیم-تحریر کی کارروائیاں پروسیسر کی سطح پر ایک ناقابل تقسیم فعل کے طور پر انجام پائیں۔
import java.util.concurrent.atomic.AtomicInteger
class SafeCounter {
private val counter = AtomicInteger(0)
fun increment() {
counter.incrementAndGet() // ایٹمی کارروائی
}
fun getCount(): Int = counter.get()
}
ڈیٹا ریس — Race Condition کی سب سے عام قسم۔ اس وقت پیدا ہوتی ہے جب ایک تھریڈ متغیر میں ڈیٹا لکھتا ہے، جبکہ دوسرا بیک وقت بغیر ہم آہنگی کے اسی متغیر کو پڑھتا یا لکھتا ہے۔ Java Memory Model میں ایسا رویہ غیر متعین سمجھا جاتا ہے — تھریڈ CPU سطح پر کیشنگ کی وجہ سے غیر متعلقہ قدر دیکھ سکتا ہے۔
Check-Then-Act پیٹرن — وہ صورت حال جب تھریڈ شرط چیک کرتا ہے اور پھر اس چیک کی بنیاد پر کارروائی کرتا ہے۔ چیک اور کارروائی کے درمیان دوسرا تھریڈ حالت بدل سکتا ہے۔ عام مثال: مجموعہ میں عنصر کی موجودگی چیک کرنا اور پھر اسے حذف کرنا۔ Android میں یہ SharedPreferences یا DB کے ساتھ کام کرتے وقت اکثر ہوتا ہے۔
Read-Modify-Write — وہ صورت حال جب تھریڈ قدر پڑھتا ہے، مقامی میموری میں ترمیم کرتا ہے اور واپس لکھتا ہے۔ اگر پڑھائی اور تحریر کے درمیان کسی دوسرے تھریڈ نے اصل قدر بدل دی، تو ترمیم کا نتیجہ کھو جائے گا۔ کلاسک مثال — counter++ کارروائی، جس کا اوپر Kotlin کوڈ میں تجزیہ کیا گیا۔
Software Transactional Memory (STM) — نقطہ نظر جہاں مشترکہ ڈیٹا پر کارروائیاں ڈیٹابیس کی طرح ٹرانزیکشنز میں انجام پاتی ہیں۔ اگر دو ٹرانزیکشنز متصادم ہوں، تو ایک رول بیک کرتی ہے اور دہرائی جاتی ہے۔ JVM کے لیے Kotlin میں Multiverse STM لائبریری دستیاب ہے جو واضح لاک کے بغیر رسائی کے تصادم کو خود بخود سنبھالتی ہے۔ STM خاص طور پر Android میں متعدد باہم مربوط آبجیکٹس کے ساتھ کام کرتے وقت مفید ہے۔
Race Condition کی ایک خاص قسم — پتلی ریس (thin races)، Activity کی زندگی کے چکر سے متعلق۔ عام منظرنامہ: پس منظر کا تھریڈ ڈیٹا لوڈ کرنا مکمل کرتا ہے، لیکن Activity پہلے ہی ختم ہو چکی ہے (اسکرین گھومنے)۔ کوروٹین غیر موجود View کو اپ ڈیٹ کرنے کی کوشش کرتی ہے اور IllegalStateException کے ساتھ کریش ہوتی ہے۔ حل — viewModelScope اور Lifecycle-aware اجزاء کا استعمال جو Lifecycle Owner کے ختم ہونے پر خود بخود کوروٹین کو منسوخ کر دیتے ہیں۔
Race Condition کا پتہ لگانا ملٹی تھریڈڈ ایپلیکیشنز کی ڈیبگنگ میں سب سے مشکل کاموں میں سے ایک ہے۔ معیاری جانچ شاذ و نادر ہی ریس کنڈیشن کو ظاہر کرتی ہے کیونکہ یہ صرف مخصوص وقتی مطابقت پر ظاہر ہوتی ہے۔ Google (Android Testing Guide, 2023) کے مطابق، تقریباً 70% Race Condition یونٹ ٹیسٹوں کے ذریعے ٹیسٹنگ ماحول میں متعین عمل درآمد ترتیب کی وجہ سے دریافت نہیں ہوتیں۔
پتہ لگانے کے اہم طریقوں میں خصوصی اوزار شامل ہیں۔ ThreadSanitizer (TSan) — Android NDK میں نصب ایک متحرک تجزیہ کار، جو میموری کی تمام رسائیوں کو ٹریک کرتا ہے اور غیر ہم آہنگ رسائی کا پتہ لگاتا ہے۔ Java/Kotlin کوڈ کے لیے، Google StrictMode کے ساتھ Android Studio Layout Inspector کی سفارش کرتا ہے، جو پس منظر کے تھریڈز سے UI-تھریڈ میں غیر قانونی رسائی کو روکتا ہے۔
ایک اور مؤثر نقطہ نظر — سٹریس ٹیسٹنگ بوجھ کے تحت بار بار ٹیسٹ چلا کر۔ JetBrains کا Lincheck فریم ورک خاص طور پر JVM پر ہم وقت ساز ڈیٹا ڈھانچوں کی جانچ کے لیے تیار کیا گیا ہے۔ یہ خود بخود کارروائیوں کی مختلف ترتیبوں کے ساتھ منظرنامے پیدا کرتا ہے اور ہر کیس میں نتائج کی درستگی کی جانچ کرتا ہے۔
| آلہ | پلیٹ فارم | تجزیہ کی قسم |
|---|---|---|
| ThreadSanitizer | Android NDK | متحرک میموری تجزیہ |
| Intel Inspector | Windows | جامد + متحرک |
| Lincheck | JVM / Kotlin | سٹریس ٹیسٹنگ |
| StrictMode | Android | رن ٹائم روکنا |
ایٹمی متغیرات (AtomicInteger, AtomicLong, AtomicReference) — اکیلی کارروائیوں کے لیے ڈیٹا ریس کو ختم کرنے کا سب سے آسان طریقہ۔ یہ پروسیسر کی نچلی سطح کی CAS ہدایات (Compare-And-Swap) استعمال کرتے ہیں جو لاک کے بغیر ایٹمی طور پر انجام پاتی ہیں۔ یہ کم مسابقت والے منظرناموں میں زیادہ سے زیادہ کارکردگی دیتا ہے۔
Mutex اور لاک — کلاسک ہم آہنگی میکانزم، پیچیدہ کارروائیوں اور نازک حصوں کے لیے موزوں۔ کوروٹین کے لیے Kotlin میں، kotlinx.coroutines لائبریری سے suspending Mutex استعمال کیا جاتا ہے جو تھریڈ کو لاک کرنے کے بجائے معطل کرتا ہے۔ یہ روایتی لاک کی خصوصیت والے بیکار انتظار سے بچاتا ہے۔
حالت کی علیحدگی — آرکیٹیکچرل نقطہ نظر جہاں ہر تھریڈ ڈیٹا کی اپنی کاپی کے ساتھ کام کرتا ہے۔ موبائل ڈیولپمنٹ میں یہ Actor-ماڈل کے ذریعے حاصل کیا جاتا ہے، جہاں ہر ایکٹر اپنی حالت کا مالک ہوتا ہے اور دوسرے ایکٹرز کے ساتھ پیغامات کا تبادلہ کرتا ہے۔ Kotlin Coroutines Channel اور SendChannel کے ذریعے Actor کا نفاذ فراہم کرتی ہے، جو آرکیٹیکچر کی سطح پر Race Condition کو مکمل طور پر ختم کرتی ہے۔
تحفظ کی ایک اضافی سطح — Immutability: اگر مشترکہ ڈیٹا اصولی طور پر ناقابل تبدیلی ہے، تو ہم آہنگی کے بغیر بھی Race Condition ناممکن ہو جاتی ہے۔ Kotlin میں اس کے لیے val-فیلڈ والے data class اور kotlinx.collections.immutable کے مجموعے استعمال کیے جاتے ہیں، جو تھریڈز کے درمیان نشر کرنے پر ڈھانچے کی ناقابل تبدیلی کو یقینی بناتے ہیں۔
اکثر پوچھے گئے سوالات
Data Race — Race Condition کی ایک مخصوص قسم ہے جہاں دو تھریڈز بیک وقت ایک ہی میموری تک رسائی کرتے ہیں، اور کم از کم ایک تحریر کرتا ہے۔ Race Condition — ایک وسیع تر تصور ہے جس میں تھریڈ کے عمل درآمد کی ترتیب پر منحصر کوئی بھی خرابی شامل ہے، بشمول منطقی ریس کنڈیشن۔
مکمل طور پر ختم کرنا ناممکن ہے، لیکن کم سے کم کیا جا سکتا ہے۔ ناقابل تبدیل آبجیکٹ (immutable)، ایٹمی اقسام اور ایک تھریڈ والے ڈسپیچر والی کوروٹین استعمال کریں۔ ThreadSafety اصول کے ساتھ Android Lint جیسے جامد تجزیہ کے اوزار مرتب کرنے کے مرحلے پر ممکنہ ریس کا پتہ لگانے میں مدد کرتے ہیں۔
UI ایپلیکیشنز میں Race Condition اکثر اسکرین کے جھلملانے، ڈیٹا کی غلط نمائش یا فہرست کو اپ ڈیٹ کرتے وقت کریش کے طور پر ظاہر ہوتی ہے۔ عام منظرنامہ: پس منظر کا تھریڈ ڈیٹا لوڈ کرتا ہے اور اڈیپٹر کو اپ ڈیٹ کرتا ہے، جبکہ صارف اس وقت فہرست کو اسکرول کرتا ہے — Adapter DataSet تک بیک وقت رسائی پیدا ہوتی ہے۔
volatile تھریڈز کے درمیان تبدیلیوں کی نمائش کو یقینی بناتا ہے — volatile متغیر میں تحریر فوری طور پر تمام تھریڈز کو دکھائی دیتی ہے۔ تاہم، volatile Read-Modify-Write اور Check-Then-Act کے مسئلے کو حل نہیں کرتا، کیونکہ یہ مرکب کارروائیوں کی ایٹمیت کو یقینی نہیں بناتا۔ ایسے منظرناموں کے لیے لاک یا ایٹمی کلاسز کی ضرورت ہوتی ہے۔
Kotlin Coroutines میں، Race Condition OS تھریڈ شیڈیولر کے بجائے کوروٹین شیڈیولر کی سطح پر پیدا ہوتی ہے۔ کوروٹین معطلی (suspend) کے مقامات پر سوئچ ہو سکتی ہیں، جو ریس کے لیے اضافی مواقع پیدا کرتی ہیں۔ kotlinx.coroutines.debug ٹول اور IntelliJ IDEA ڈیبگر کوروٹین کی حالت کو ٹریک کرنے میں مدد کرتے ہیں۔
خلاصہ
ہم ایک موبائل ایپلیکیشن ٹرنکی تیار کریں گے
IT Sectr 2017 سے اسٹارٹ اپس اور کاروبار کے لیے iOS اور Android ایپلیکیشنز بناتا ہے۔ ہم آپ کو مشورہ دیں گے اور بہترین حل تجویز کریں گے۔
مزید پڑھیں