Какие основные проблемы возникают при работе с многопоточностью?

Ответ

Работа с несколькими потоками сопряжена с рядом классических проблем:

Проблема Описание Пример риска в Swift/iOS
Состояние гонки (Race Condition) Неопределённый результат из-за неконтролируемого порядка доступа потоков к общим данным. Неатомарное изменение общего счётчика из нескольких потоков приводит к потере обновлений.
Взаимная блокировка (Deadlock) Два или более потока бесконечно ожидают освобождения ресурсов, заблокированных друг другом. Синхронный вызов (sync) на текущей serial очереди или циклическая блокировка мьютексов.
Инверсия приоритетов (Priority Inversion) Низкоприоритетный поток удерживает ресурс (например, блокировку), нужный высокоприоритетному, блокируя его выполнение. Может происходить при использовании общих DispatchQueue или NSLock с разными QoS.
Голодание (Starvation) Поток не может получить доступ к необходимым ресурсам и не прогрессирует. Долгие задачи на serial очереди или неправильная настройка приоритетов в OperationQueue.
Проблемы с памятью Утечки, висячие указатели из-за неправильного управления жизненным циклом объектов между потоками. Сильные ссылки в захватах замыканий, удерживающие объекты в фоновых потоках.

Конкретные примеры и решения:

  • Гонка данных:
    var unsafeCounter = 0
    DispatchQueue.concurrentPerform(iterations: 1000) { _ in
        unsafeCounter += 1 // НЕБЕЗОПАСНО! Итоговое значение будет меньше 1000.
    }
    // Решение: Использовать изолированный актор или очередь с барьером.
  • Обновление UI не из главного потока:
    DispatchQueue.global().async {
        self.label.text = "New Text" // КРАШ или неопределённое поведение.
    }
    // Решение: Всегда обновлять UI через `DispatchQueue.main` или `MainActor`.

Основные инструменты предотвращения: Акторы (actor), семафоры (DispatchSemaphore), очереди с барьерами (DispatchQueue.barrier), атомарные операции, а также архитектурные паттерны, минимизирующие общее состояние.

Ответ 18+ 🔞

О, смотри-ка, классика жанра! Многопоточность — это как вечеринка в общаге, куда навалилось дохуя народа, а холодильник с пивом один. И все хотят туда одновременно. Вот и начинается пиздец.

Смотри, какие тут подлянки могут вылезти:

Проблема Что это за хуйня? Где на iOS/Swift подстерегает, как маньяк в подворотне
Состояние гонки Два потока как два мудака несутся к одной переменной. Кто первый добежит и нагадит — тот и молодец. Результат каждый раз разный, ебать его в сраку. Счётчик, который меняют с разных очередей без защиты. В итоге вместо 1000 обновлений получаем 950, и где ещё 50 — хер его знает.
Взаимная блокировка Это когда два потока встали в позу "ты мне — я тебе" и ждут друг от друга ресурс. Ждут, ждут, и оба нахуй сдохнут. Сделал sync на той же самой serial очереди, на которой уже стоишь — и привет, вечный сон. Или два лока друг за другом ухватились.
Инверсия приоритетов Представь: какой-то ленивый засранец-поток с низким приоритетом запер дверь в сортир (захватил лок). А тут срочно приспичило потоку с высоким приоритетом, прям вот щас. И он стоит, дёргает ручку, а тот внутри сидит и в телефон играет. Система в ауте. Используешь общую DispatchQueue для задач с разным QoS — и вот тебе, блядь, сюрприз.
Голодание Какой-нибудь бедолага-поток вечно в конце очереди. Все ресурсы расхватали, а ему нихуя не перепадает. Сидит, сука, грустит. Долбаёб засунул на serial очередь десятиминутную задачу, а за ней стоят ещё сто мелких. Все сто будут ждать, пока этот мудак не отсосёт.
Проблемы с памятью Объекты начинают жить своей жизнью, плодятся где не надо, не умирают, когда должны. Короче, бардак и утечки. Классика: в замыкании для фоновой задачи захватил self сильной ссылкой, а потом удивляешься, почему контроллер не умирает.

Конкретные примеры, где всё идёт по пизде:

  • Гонка данных, мать её:

    var unsafeCounter = 0
    DispatchQueue.concurrentPerform(iterations: 1000) { _ in
        unsafeCounter += 1 // ОПАСНОСТЬ, НЕ ПОВТОРЯЙ! Итоговое значение будет меньше 1000.
    }
    // Решение: Использовать изолированный актор или очередь с барьером.

    Вот тут, блядь, 1000 потоков как тараканы кинутся к одной переменной. += — это же не атомарная операция! Прочитали одновременно значение 5, оба прибавили 1, оба записали 6. И одно обновление нахуй сгорело. Так миллион раз.

  • Обновление UI с заднего двора:

    DispatchQueue.global().async {
        self.label.text = "New Text" // КРАШ или неопределённое поведение.
    }
    // Решение: Всегда обновлять UI через `DispatchQueue.main` или `MainActor`.

    Это, ёпта, святое правило! UI-шные штуки трогать можно ТОЛЬКО из главного потока. Иначе приложение взбрыкнёт и умрёт с красивым крешем. Не будь мудаком — оберни в DispatchQueue.main.async.

Чем от всего этого спасаться? Ну, бля, инструментов дохуя: акторы (actor), семафоры (DispatchSemaphore), очереди с барьерами (DispatchQueue.barrier), атомарные операции. Но главное — архитектура. Лучшая синхронизация — это когда синхронизировать нехуй. Сделай так, чтобы общих изменяемых данных было минимум, и жизнь станет проще.