Почему вызов DispatchQueue.main.sync из главного потока приводит к deadlock?

Ответ

Вызов DispatchQueue.main.sync из того же самого (главного) потока вызывает взаимную блокировку (deadlock) из-за принципа работы синхронных операций и очередей.

Механизм deadlock:

  1. sync блокирует текущий поток и ждет, пока переданный блок кода выполнится.
  2. Блок кода ставится в очередь (DispatchQueue.main) на выполнение.
  3. Если очередь уже занята этим же потоком (главный поток выполняет наш sync-вызов), она не может начать выполнение нового блока, пока не освободится.
  4. Возникает циклическая зависимость: Поток ждет очередь → Очередь ждет поток.

Пример deadlock:

// Вызов из главного потока:
DispatchQueue.main.sync {
    // Этот блок никогда не будет выполнен
    updateUI()
}
// Поток заблокирован здесь навсегда

Решение: Всегда используйте DispatchQueue.main.async для планирования работы на главной очереди из самого главного потока.

// Правильно:
DispatchQueue.main.async {
    self.updateUI()
}

Исключение: Вызов sync из другого (фонового) потока на главную очередь — безопасен и является стандартным паттерном для синхронизации результатов с UI.

Ответ 18+ 🔞

Да ты посмотри, какая интересная хуйня вырисовывается! Вот сидишь ты на главном потоке, царь-батюшка, и вдруг решаешь: "А вызову-ка я DispatchQueue.main.sync!" И всё, приехали. Завис навечно, как дурак.

А че так-то, спросишь?

  1. Эта команда sync — она жадная сука. Она хватает текущий поток в ебучую мертвую хватку и не отпускает, пока не выполнится тот кусок кода, который ты ей всунул.
  2. Код-то твой она аккуратненько ставит в очередь, в самую главную, DispatchQueue.main.
  3. А очередь эта уже занята! Кем? Да тем самым потоком, который сейчас в истерике держится за sync и орёт "Выполняйся давай!".
  4. И получается пиздецовая круговая порука: Поток орёт на очередь, мол, выполни мой блок! А очередь в ответ: да я и так тебя обслуживаю, отпусти меня сначала, мудак! И оба ждут друг друга до скончания веков. Classic deadlock, ебать его в сраку.

Вот тебе наглядный пиздец:

// Сидишь ты уже на главном потоке и делаешь это:
DispatchQueue.main.sync {
    // Этот код — как мечта идиота. Он никогда не случится.
    updateUI()
}
// А поток твой намертво застрял вот тут, навеки вечные.

Что делать, чтобы не быть таким мудаком? Да всё просто, как три копейки! Хочешь что-то кинуть на главный поток из самого главного потока — используй async. Он не жадный, он вежливый: поставит задачу в очередь и пойдёт дальше по своим делам, не блокируя всех и вся.

// Делай так — и будет тебе счастье:
DispatchQueue.main.async {
    self.updateUI()
}

А когда sync тогда нужен, спросит дотошный читатель? А вот когда ты с какого-нибудь фонового потока, с задворков, приполз с результатами и хочешь синхронно, прямо щас, обновить интерфейс. Тогда — да, DispatchQueue.main.sync с фонового потока — это святое, рабочий паттерн. Но из главного в главный — это путь в никуда, в рот меня чих-пых!