هر برنامه موبایل همزمان کارهای زیادی انجام میدهد: بارگذاری داده از شبکه، پردازش لمس کاربر، انیمیشن رابط و ذخیره فایلها. اگر تمام این کد در یک ریسه اجرا شود، برنامه با هر تأخیر شبکه هنگ میکند. چندریسندگی (multithreading) و همروندی مفاهیم کلیدی هستند که به برنامه اجازه میدهند پاسخگو و کارآمد باقی بماند. در این مقاله تمام ابزارهای اصلی را بررسی خواهیم کرد: از Main Thread و RunLoop تا کوروتینهای Kotlin و Combine در iOS. مطالب بر اساس مستندات رسمی Apple GCD است.
نکات کلیدی
چندریسندگی توانایی یک برنامه برای اجرای همزمان چندین قطعه کد است. هر قطعه در یک ریسه (Thread) جداگانه اجرا میشود — یک فرآیند سبک با پشته فراخوانی خود. در توسعه موبایل، ریسهها به دو دسته تقسیم میشوند: Main Thread (ریسه UI) و Background Threads (ریسههای پسزمینه).
سیستم عامل خود توزیع ریسهها را بین هستههای پردازنده مدیریت میکند. دستگاههای مدرن دارای 6–8 هسته هستند، بنابراین اجرای موازی میتواند کار را سرعت بخشد. با این حال، ایجاد ریسه یک عملیات پرهزینه است، بنابراین کار مستقیم با Thread توصیه نمیشود. در عوض، از انتزاعات سطح بالاتر استفاده میشود: DispatchQueue، OperationQueue، CoroutineDispatcher.
همروندی (Concurrency) مفهومی گستردهتر از چندریسندگی است. همروندی به این معنی است که کارها حتی روی یک هسته واحد از طریق تغییر زمینه میتوانند "همزمان" اجرا شوند. ناهمگامی (Async/Await) یک مدل برنامهنویسی است که در آن یک کار ریسه را مسدود نمیکند، بلکه در حین انتظار برای نتیجه، کنترل را برمیگرداند. زبانهای مدرن (Kotlin، Swift، Dart) پشتیبانی داخلی از Async/Await دارند.
در IT Sectr، ما به معماری صحیح چندریسندگی در شروع پروژه توجه ویژهای داریم. اشتباهات در مراحل اولیه منجر به باگهای دشوار میشود: رقابت دادهها، بنبست و ناپایداری برنامه تحت بار. هر یک از پروژههای ما در مرحله برنامهریزی تحت بررسی معماری همروندی قرار میگیرد.
Main Thread (ریسه اصلی) — تنها ریسه در یک برنامه موبایل است که به UI دسترسی دارد. در Android به آن UI Thread گفته میشود، در iOS — Main Thread. تمام عملیات رابط — تغییر متن، انیمیشن، پردازش لمس — فقط در Main Thread انجام میشود. اگر یک عملیات سنگین (بارگذاری فایل، تجزیه JSON) روی ریسه اصلی انجام شود، رابط از پاسخ دادن بازمیایستد. در Android این منجر به ANR (Application Not Responding) میشود، در iOS — صفحه "یخ میزند".
Background Threads (ریسههای پسزمینه) برای هر چیزی که به UI مربوط نیست در نظر گرفته شدهاند: درخواستهای شبکه، عملیات پایگاه داده، پردازش تصویر، رمزنگاری. پس از اتمام، نتیجه برای نمایش به Main Thread منتقل میشود. هر پلتفرم ابزارهای خود را برای جابجایی بین ریسهها فراهم میکند: DispatchQueue.main.async در iOS، runOnUiThread یا withContext(Dispatchers.Main) در Android.
RunLoop — حلقه پردازش رویداد در ریسه اصلی iOS. RunLoop منتظر رویدادها (لمس، تایمر، اعلانها) میماند و آنها را به کنترلکنندههای مناسب ارسال میکند. در Android معادل آن Looper است که با هر Main Thread مرتبط است. Main Looper بینهایت پیامها را از صف استخراج کرده و برای پردازش به Handler تحویل میدهد. درک RunLoop و Looper به جلوگیری از نشت حافظه و "لکنت" رابط کمک میکند.
Grand Central Dispatch (GCD) — کتابخانه اپل برای مدیریت چندریسندگی در سطح زبان C است. GCD با DispatchQueue — صفهای کار کار میکند. توسعهدهنده به صورت دستی ریسه ایجاد نمیکند؛ GCD یک استخر ریسه (Thread Pool) را مدیریت کرده و کارها را بین هستههای موجود پردازنده توزیع میکند. DispatchQueue دو نوع دارد: Serial Queue (صف ترتیبی — کارها یکی پس از دیگری اجرا میشوند) و Concurrent Queue (صف همزمان — کارها میتوانند همزمان اجرا شوند).
Main DispatchQueue — یک صف ترتیبی است که به ریسه اصلی متصل است. Global Queues — صفهای همزمان با اولویتهای مختلف (QoS — Quality of Service) هستند: userInteractive، userInitiated، utility، background. انتخاب QoS صحیح برای عملکرد حیاتی است: .userInteractive — برای کارهایی که بر UI تأثیر میگذارند (انیمیشنها، رندرینگ)؛ .background — برای کارهای غیرحساس به زمان (همگامسازی، پاکسازی حافظه پنهان).
OperationQueue — انتزاعی بر روی GCD با قابلیتهای اضافی: لغو کارها، تنظیم وابستگیها بین عملیات، کنترل حداکثر تعداد عملیات همزمان. عملیاتها اشیاء کلاس Operation (یا BlockOperation) هستند. مثال: اگر نیاز به بارگذاری یک تصویر، سپس اعمال فیلتر و فقط پس از آن نمایش آن دارید — OperationQueue با وابستگیها به خوبی از عهده آن برمیآید. در GCD باید این مراحل را با استفاده از DispatchGroup یا سمافور به صورت دستی همگامسازی کنید.
Async/Await در Swift 5.5+ — جایگزین مدرن برای GCD است. کلمات کلیدی async و await کد ناهمگام را خطی و خوانا میکنند. توابع به عنوان async علامتگذاری میشوند و فراخوانیها از طریق await منتظر میمانند. سیستم خود تغییر زمینه را مدیریت میکند: به طور پیشفرض، یک تابع async روی ریسه پسزمینه اجرا میشود، در حالی که بهروزرسانی UI روی MainActor اجرا میشود. @MainActor — ویژگیای است که اجرای کد را روی ریسه اصلی تضمین میکند.
Coroutines (کوروتینها) — ریسههای سبکی برای Kotlin هستند که توسط JetBrains توسعه یافتهاند. برخلاف ریسههای معمولی، کوروتینها به یک Thread خاص متصل نیستند. هزاران کوروتین میتوانند بدون سربار قابل توجه روی چندین ریسه اجرا شوند. CoroutineScope چرخه حیات کوروتینها را مدیریت میکند: viewModelScope به ViewModel متصل است، lifecycleScope — به Activity/Fragment. هنگامی که محدوده از بین میرود، تمام کوروتینهای فرزند به طور خودکار لغو میشوند.
Dispatchers تعیین میکنند کوروتین روی کدام استخر ریسه اجرا شود: Dispatchers.Main — ریسه UI؛ Dispatchers.IO — برای درخواستهای شبکه و عملیات دیسک؛ Dispatchers.Default — برای محاسبات سنگین CPU. برای تغییر dispatcher از withContext استفاده میشود. کوروتینها از همروندی ساختاریافته (structured concurrency) پشتیبانی میکنند: هر کوروتین یک والد دارد و هنگام لغو والد، تمام کوروتینهای فرزند لغو میشوند. این از نشت حافظه و وظایف معلق جلوگیری میکند.
Flow — یک جریان داده ناهمگام سرد از کتابخانه کوروتین است. Flow مقادیر را به ترتیب منتشر میکند: (1) تولیدکننده داده تولید میکند، (2) عملگرها جریان را تبدیل میکنند، (3) جمعکننده نتیجه را مصرف میکند. برخلاف LiveData، Flow از زنجیرههای عملگر پیچیده (map, filter, flatMapConcat, catch) پشتیبانی میکند و کاملاً ایمن برای ریسه است. StateFlow و SharedFlow — انواع داغ Flow هستند، برای وضعیت UI و رویدادهای یک بار (Snackbar، ناوبری) ایدهآل هستند.
Channel — انتزاع دیگری از کوروتین برای انتقال داده بین کوروتینها. Channel مانند یک صف کار میکند: یک فرستنده (send) و یک یا چند گیرنده (receive). کانالهای بافر شده (Channel(UNLIMITED)، Channel(BUFFERED)) امکان پیکربندی رفتار در هنگام سرریز را فراهم میکنند. Channel اغلب با Flow برای پل زدن APIهای مبتنی بر callback به کوروتینها استفاده میشود: callbackFlow { … }.
در IT Sectr ما به طور فعال از کوروتینها و Flow در تمام پروژههای Android استفاده میکنیم. این امکان نوشتن کد ناهمگام را فراهم میکند که همگام به نظر میرسد، به راحتی تست میشود (runTest, TestDispatcher) و نیاز به مدیریت دستی ریسه ندارد. مثال یک کوروتین ساده با بارگذاری داده:
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUsers(): List<User> = withContext(Dispatchers.IO) {
return@withContext try {
val users = api.fetchUsers()
dao.insertAll(users)
users
} catch (e: Exception) {
dao.getAll()
}
}
}
برنامهنویسی واکنشگرا — پارادایمی است که در آن دادهها به عنوان جریانهای ناهمگام (Observable, Publisher) منتشر میشوند. RxJava/RxKotlin — محبوبترین پیادهسازی برای Android است که از .NET Rx منتقل شده است. RxSwift — کتابخانه مشابهی برای iOS است. اجزای اصلی: Observable (منبع رویداد)، Observer (مشترک)، Scheduler (مدیریت ریسه)، Operators (تبدیل جریان).
Combine — چارچوب اپل برای برنامهنویسی واکنشگرا است که در iOS 13 معرفی شد. Combine از پروتکلهای Publisher (ناشر) و Subscriber (مشترک) استفاده میکند. برخلاف RxSwift، Combine در SDK تعبیه شده و با SwiftUI ادغام نزدیکی دارد. عملگرها در Combine: map, filter, combineLatest, zip, debounce, throttle — بیشتر سناریوها را پوشش میدهند: از اتصال داده به UI تا debounce جستجو.
Future و Promise — الگوهایی برای کار با یک نتیجه ناهمگام واحد. Future مقداری را نشان میدهد که بعداً در دسترس خواهد بود. Promise قول ارائه مقدار است. در Rx این Single (یک پاسخ موفق یا خطا) است، در Combine — Future Publisher. در عمل، Future/Promise برای درخواستهای تکی API مناسب هستند، در حالی که Observable/Publisher — برای جریانهای پیوسته (مکان یابی، ورود متن).
Callback و Delegate — الگوهای کلاسیک برای عملیات ناهمگام. Callback — تابعی که به عنوان آرگومان ارسال میشود و در پایان عملیات فراخوانی میشود. Delegate — شیئی که پروتکلی با متدهای مدیریت رویداد پیادهسازی میکند. اشکال: "جهنم callback" (callbackهای تو در تو) و پیچیدگی مدیریت خطا. NotificationCenter (iOS) و EventBus (Android) — مکانیزمهای پخش رویداد، مفید برای ارتباط ضعیف اما منجر به وابستگیهای ضمنی.
چندریسندگی در را به عملکرد بالا میگشاید، اما همزمان خطر خطاهای دشوار را ایجاد میکند. رایجترین: Race Condition (شرایط رقابت)، Deadlock (بنبست)، Livelock (قفل زنده) و Starvation (گرسنگی ریسه). درک این مشکلات یک مهارت ضروری برای هر توسعهدهنده موبایل است.
Race Condition زمانی رخ میدهد که دو یا چند ریسه بدون همگامسازی همزمان دادههای یکسان را میخوانند و مینویسند. نتیجه بستگی به این دارد که کدام ریسه اول اجرا شود. مثال کلاسیک: دو ریسه یک شمارنده را افزایش میدهند. عملیات "خواندن → افزایش → نوشتن" اتمیک نیست، بنابراین هنگام اجرای همزمان، یک افزایش "از دست میرود". راهحل — استفاده از عملیات اتمیک (AtomicInteger, AtomicReference) یا قفلها (Mutex, Semaphore, synchronized).
Deadlock — وضعیتی که هر ریسه یک منبع را نگه میدارد و منتظر منبعی است که توسط ریسه دیگری گرفته شده است. هیچ ریسهای نمیتواند ادامه دهد. شرایط وقوع: انحصار متقابل، نگهداری و انتظار، بدون تقدم، انتظار حلقوی. پیشگیری: ایجاد یک ترتیب واحد برای تصاحب قفلها، استفاده از tryLock با زمان انتظار، اعمال الگوریتمهای Lock-Free (ConcurrentHashMap, CopyOnWriteArrayList).
Livelock — ریسهها مسدود نیستند اما بدون انجام کار مفید مداوم منابع را به یکدیگر "انتقال" میدهند. مثال: دو نفر در راهرو به هم میرسند و هر دو در یک جهت حرکت کرده راه را باز میکنند. Starvation — یک ریسه به منبع دسترسی پیدا نمیکند زیرا ریسههای دیگر مدام آن را رهگیری میکنند. راهحل: قفلهای عادلانه (fair locks)، اولویتهای ریسه با احتیاط.
برای جلوگیری از مشکلات چندریسندگی از اولیههای همگامسازی استفاده میشود: Mutex (انحصار متقابل)، Semaphore (محدود کردن تعداد دسترسیهای همزمان)، Lock (رابط با tryLock)، Synchronized (قفل سطح JVM)، @MainActor (Swift — تضمین اجرا روی ریسه اصلی). در Android همچنین ThreadPool از طریق Executors.newFixedThreadPool، newCachedThreadPool در دسترس است. با این حال، مدیریت دستی استخرها prerogative پروژههای قدیمی است؛ در پروژههای جدید بهتر است از کوروتینها استفاده شود.
| ابزار | پلتفرم | نوع | ویژگیها |
|---|---|---|---|
| DispatchQueue (GCD) | iOS | صف کار | ترتیبی/همزمان، اولویتهای QoS، استخر ریسه مدیریت شده توسط سیستم |
| OperationQueue | iOS | صف عملیات | وابستگیها، لغو، maxConcurrentOperationCount |
| Coroutines + Flow | Android | کوروتین | سبک، همروندی ساختاریافته، StateFlow، Channel |
| RxJava / RxKotlin | Android | جریان واکنشگرا | Observable، Schedulers، مجموعه عملگر غنی |
| Combine | iOS | جریان واکنشگرا | Publisher/Subscriber، ادغام با SwiftUI |
| Async/Await + Task | iOS / Android | مدل ناهمگام | کد خطی، @MainActor، همروندی ساختاریافته |
سوالات متداول
Main Thread (ریسه UI) مسئول رندرینگ رابط و پردازش لمس است. Background Thread وظایف پسزمینه را انجام میدهد — بارگذاری داده، محاسبات، کار شبکه. مسدود کردن Main Thread باعث یخ زدن رابط میشود (ANR در Android، frozen UI در iOS).
Race Condition — شرایط رقابت زمانی که دو ریسه به طور همزمان به دادههای مشترک دسترسی پیدا میکنند و نتیجه به ترتیب اجرا بستگی دارد. از طریق همگامسازی جلوگیری میشود: Mutex, Semaphore, Lock, Synchronized، @MainActor یا عملیات اتمیک.
Coroutines استاندارد مدرن برای Android است (JetBrains، پشتیبانی شده توسط Google). RxJava/RxKotlin رویکرد واکنشگرا با مجموعه عملگر غنی است. Coroutines برای فراخوانیهای ناهمگام سادهتر است، RxJava برای جریانهای داده پیچیده قدرتمندتر است. در IT Sectr ما برای پروژههای جدید از Coroutines + Flow استفاده میکنیم.
Deadlock — بنبست متقابل که در آن دو ریسه منتظر منابع یکدیگر هستند. Livelock — ریسهها مسدود نیستند اما بدون کار مفید مداوم منابع را انتقال میدهند. هر دو مشکل با ترتیب قفل مناسب و زمان انتظار حل میشوند.
DispatchQueue انتزاعی از Grand Central Dispatch (GCD) برای مدیریت ریسه است. Main Queue وظایف را روی ریسه اصلی اجرا میکند، Global Queues — روی ریسههای پسزمینه. Serial Queue اجرای ترتیبی را تضمین میکند، Concurrent Queue — موازی. در پروژههای مدرن، GCD اغلب با Async/Await و Task جایگزین میشود.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.