בתרגול שלנו, deadlock באפליקציית iOS שוכפל בצורה לא אמינה: פעם ב-20–30 דקות האפליקציה הייתה קופאת לחלוטין. יומני הקריסה לא הראו דבר כי זו לא קריסה—זה deadlock. דומם מצב ה-thread דרך Xcode חשף: ה-main thread חסום על DispatchQueue.sync ל-SerialQueue, והתור הזה חיכה ל-completion handler שניסה לרוץ על ה-main thread. deadlock קלאסי של שני threads. באגי concurrency כאלה הם בין היקרים ביותר בפיתוח מובייל: הם נדירים, משתכפלים באופן ספורדי, ולעיתים קרובות מגיעים לייצור. במשך 5 שנים, ניתחנו יותר מ-50 פרויקטים עם בעיות דומות. חיסכון בזמן על דיבאגינג של concurrency יכול להגיע ל-60%, וחיסכון בתקציב עד 30% בזכות פחות תקלות.
Concurrency הוא אחד הנושאים הקשים ביותר. Data races, deadlocks, עדכוני UI שלא מה-main thread—הבאגים האלה מופיעים לעיתים רחוקות והם יקרים. אנו משתמשים בכלים מודרניים: Swift Concurrency, Kotlin Coroutines, Thread Sanitizer כדי לחסל אותם בשלב הפיתוח. Structured concurrency (async/await) משפר את קריאות הקוד פי 3 ומפחית את ההסתברות ל-races פי 2 בהשוואה ל-GCD.
כיצד אבחון threads עוזר לייעל concurrency?
תסמינים אופייניים: UI קופא לשניות או לנצח, האפליקציה לא מגיבה למגע. בניגוד לקריסה, deadlock לא מייצר יומן קריסה (ב-40% מהמקרים התסמין הראשון הוא תלונת משתמש). אבחון דורש כלים מיוחדים. ב-iOS — Thread Sanitizer (TSan) ב-Xcode: הוא מזהה data races אבל לא את כל ה-deadlocks. תיעוד אפל מציין: Thread Sanitizer מזהה data races במהלך ביצוע. עבור deadlocks אנו משתמשים ב-Instruments → Time Profiler: רואים אילו threads חסומים ועל אילו תורים. באנדרואיד — Android Studio Profiler → Threads: צפייה במצבים RUNNABLE, WAIT, BLOCKED. StrictMode תופס דיסק/רשת על ה-main thread — אנו מפעילים אותו עם SerialQueue ב-builds של דיבאג.
בעיות threading אופייניות
עדכוני UI שלא מה-main thread
באנדרואיד: penaltyFlashScreen(). הסיבה — טיפול בתגובת רשת ישירות ב-Retrofit callback ללא CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
ב-iOS: withContext(Dispatchers.Main) ב-Xcode (מופעל כברירת מחדל בהגדרות Scheme) תופס גישות ל-UIKit מ-threads ברקע ב-builds של דיבאג. ב-release — קריסות אקראיות או קלקול ויזואלי.
תבנית iOS נכונה:
DispatchQueue.global(qos: .userInitiated).async {
let result = heavyComputation()
DispatchQueue.main.async {
self.label.text = result // только здесь
}
} פיצוץ threads עם GCD
פיצוץ threads מתרחש כאשר נוצרים הרבה threads דרך GCD ללא מגבלות. המערכת מקצה threads באגרסיביות, מה שגורם לירידה חדה בביצועים תחת עומס. הפתרון הוא concurrency מוגבל דרך Main Thread Checker או דרך Swift Concurrency DispatchQueue.global(qos: .userInitiated).async { let result = heavyComputation() DispatchQueue.main.async { self.label.text = result // только здесь } } עם OperationQueue.maxConcurrentOperationCount מפורש ומקביליות מוגבלת:
await withTaskGroup(of: Result.self) { group in
for item in items.prefix(4) { // не более 4 параллельных задач
group.addTask {
await process(item)
}
}
} Data races
מספר threads קוראים וכותבים שדה ללא סנכרון. ב-Swift — Thread Sanitizer (TSan) מזהה data races ב-builds של דיבאג. הפעל ב-Scheme → Diagnostics → Thread Sanitizer.
אפשרויות סנכרון:
-
TaskGroup/withTaskGroup— mutexes מהירים לקטעים קריטיים -
await withTaskGroup(of: Result.self) { group in for item in items.prefix(4) { // не более 4 параллельных задач group.addTask { await process(item) } } }עםNSLockלתבנית read-write lock - Actor ב-Swift 5.5+ — הגישה המודרנית ביותר, הקומפיילר מבטיח בידוד נתונים
actor UserCache {
private var storage: [String: User] = [:]
func get(_ id: String) -> User? {
storage[id]
}
func set(_ user: User) {
storage[user.id] = user
}
}עם actor, הקומפיילר לא יאפשר גישה ל-os_unfair_lock מחוץ להקשר ה-actor ללא DispatchQueue(label:attributes:.concurrent). Actor אמין פי 2 מסנכרון ידני עם NSLock.
אנדרואיד: שימוש לא נכון ב-Coroutines
barrier הוא דגל אדום. ה-coroutine חי לנצח, לא מבוטל כשהמסך נסגר. בפתיחה מחדש, נוצר שני. נכון: actor UserCache { private var storage: [String: User] = [:] func get(_ id: String) -> User? { storage[id] } func set(_ user: User) { storage[user.id] = user } } (מבוטל על storage) או await (מבוטל על GlobalScope.launch).
viewModelScope.launch לעומת onCleared: כשנקרא מה-main thread lifecycleScope.launch מתבצע באופן סינכרוני ללא החלפת הקשר — חשוב לאנימציות ועדכוני UI מיידיים.
טיפול לא נכון בשגיאות ב-coroutines:
// НЕПРАВИЛЬНО — исключение не поймается
scope.launch {
try {
riskyOperation()
} catch (e: Exception) {
handle(e)
}
}
// ПРАВИЛЬНО — CoroutineExceptionHandler для структурной обработки
val handler = CoroutineExceptionHandler { _, e -> handleError(e) }
scope.launch(handler) {
riskyOperation()
} למה structured concurrency הוא הבסיס לייעול concurrency?
Structured concurrency (async/await ב-Swift, Kotlin Coroutines עם coroutine scope) מבטיח ביטול של משימות כשההקשר מסתיים, מבטל דליפות threads, ומפשט קריאת קוד. בניגוד ל-GCD/Thread, שבו קל ליצור פיצוץ threads או deadlock, structured concurrency אוכף היקף משימה מקומי. Actor ב-Swift מספק בידוד נתונים ברמת הקומפיילר, ומפחית race conditions פי 2 בהשוואה לסנכרון ידני.
כלי אבחון
| כלי | פלטפורמה | מה הוא מוצא |
|---|---|---|
| Thread Sanitizer (TSan) | iOS / אנדרואיד | Data races |
| Main Thread Checker | iOS | UI מ-thread ברקע |
| Instruments → Time Profiler | iOS | Threads חסומים |
| Android Studio Profiler → Threads | אנדרואיד | מצבי threads, שינה/חסימה/ריצה |
| StrictMode | אנדרואיד | דיסק/רשת על ה-main thread |
| Kotlin Coroutines Debugger | אנדרואיד | Coroutines פעילים, ה-stacks שלהם |
השוואת גישות סנכרון
| גישה | בטיחות | ביצועים | מורכבות |
|---|---|---|---|
| NSLock / os_unfair_lock | בינוני (ידני) | גבוה | נמוך |
| DispatchQueue concurrent + barrier | בינוני | גבוה | בינוני |
| Actor (Swift) | גבוה (קומפיילר) | בינוני | נמוך |
| Kotlin Mutex | גבוה | גבוה | בינוני |
מקרה מהתרגול שלנו: deadlock ב-Swift
אפליקציית לקוח מסחר אלקטרוני: בהוספה לעגלה, ה-UI לפעמים קפא למשך 30–60 שניות. שוכפל רק באינטרנט חלש.
דומם מצב ה-thread חשף: onDestroy קרא ל-Dispatchers.Main בתוך Dispatchers.Main.immediate, ו-Dispatchers.Main.immediate בתוך חיכה ל-// НЕПРАВИЛЬНО — исключение не поймается scope.launch { try { riskyOperation() } catch (e: Exception) { handle(e) } } // ПРАВИЛЬНО — CoroutineExceptionHandler для структурной обработки val handler = CoroutineExceptionHandler { _, e -> handleError(e) } scope.launch(handler) { riskyOperation() } , שגם הוא היה בתור לכתיבה. עם עיכוב רשת, מספר קריאות CartService.addItem() הצטברו ואחת הגיעה ל-deadlock עם userDefaults.synchronize().
פתרון: הסרנו serialQueue.sync (no-op ב-iOS 12+), העברנו שמירת עגלה לכתיבה אסינכרונית דרך synchronize(). לאחר התיקון, ה-deadlock נעלם, זמן התגובה השתפר ב-40%.
שלבי עבודה
- הפעל TSan ו-Main Thread Checker בכל ריצות הבדיקה
- נתח מצב threads ב-Instruments / תצוגת Android Profiler Threads
- בדוק את כל המקומות עם קריאות
addItem()ומצב משתנה משותף - תיקון: הפניות חלשות, תורי dispatch נכונים, בידוד actor
- בדיקות עומס לאיתור race conditions תחת עומס
מה כלול
- ביקורת concurrency מלאה עם דוח הבעיות שנמצאו
- תיקוני קוד: החלפת GCD ב-async/await, הצגת actor, אופטימיזציה של coroutines
- תיעוד השינויים והמלצות להמשך פיתוח
- גישה ל-repository של התיקונים, הדרכת צוות (עד שעתיים)
- אחריות על תיקונים — 3 חודשים לאחר המסירה
לוחות זמנים ואיך להתחיל
ביקורת concurrency — 2–4 ימים. תיקון הבעיות שנמצאו — מ-3 עד 14 ימים, תלוי בעומק השינויים הארכיטקטוניים. העלות מחושבת באופן אישי. אם אתם חושדים ב-deadlocks או race conditions — צרו קשר, נעריך את הפרויקט שלכם תוך יומיים. הזמינו ביקורת concurrency היום וקבלו דוח מפורט עם הצעות תיקון.







