Всегда ли вызов sync на main очереди приводит к deadlock?

Ответ

Нет, не всегда. Deadlock гарантированно произойдет только если sync вызывается на DispatchQueue.main из того же самого MainThread.

Механизм deadlock:

  1. sync блокирует текущий поток до завершения переданного блока.
  2. Если этот вызов делается из MainThread, поток блокируется и ждет.
  3. Поскольку MainQueue последовательная, она не может начать выполнение нового блока (того, который мы передали в sync), пока не завершит текущий (тот, из которого был вызван sync). Возникает взаимная блокировка.

Пример deadlock:

// ВЫЗОВ ИЗ MAIN THREAD -> DEADLOCK
DispatchQueue.main.sync {
    // Этот блок никогда не начнет выполняться
    print("This will never print")
}

Безопасный вызов: Если sync на DispatchQueue.main вызывается из другого потока (например, из background очереди), deadlock не произойдет. Background поток заблокируется, main поток выполнит блок и разблокирует background поток.

Пример безопасного вызова:

DispatchQueue.global().async {
    // Вызов из фонового потока — БЕЗОПАСНО
    DispatchQueue.main.sync {
        // Обновляем UI на MainThread
        self.label.text = "Updated safely"
    }
    // Код продолжит выполнение здесь после завершения блока выше
}

Правило: Никогда не вызывайте DispatchQueue.main.sync из кода, который может выполняться на MainThread.

Ответ 18+ 🔞

А, слушай, вот эта тема с дедлоком на главной очереди — это классика, прям как в анекдоте про двух баранов на мосту, блядь. Все думают, что sync на main — это всегда мгновенный пиздец, но не всё так однозначно, ёпта!

Вот смотри, в чём тут собака зарыта, а точнее, где она сдохнет. Дедлок — это гарантированный подарок судьбы только в одном, ебучем случае: когда ты вызываешь DispatchQueue.main.sync прямо с самой главной очереди, с MainThread'а.

Как это работает, или механизм самоубийства:

  1. Команда sync говорит: «Эй, поток, стой тут, не дыши, пока я не сделаю то, что в этих фигурных скобках».
  2. Если ты уже на MainThread, то этот поток и есть тот самый единственный мудак, который должен всё делать. Он встаёт как вкопанный и ждёт.
  3. А очередь main — она последовательная, сука. Она не может начать выполнять новый блок (тот, что ты в sync засунул), пока не закончит старый (тот, из которого ты этот sync и вызвал). Получается патовая ситуация: один ждёт другого, а они, блядь, один и тот же! В рот меня чих-пых, вот и дедлок.

Пример, как НЕ НАДО делать (классика жанра):

// Делаем это прямо из MAIN THREAD -> ПИЗДЕЦ, ТУПИК
DispatchQueue.main.sync {
    // Этот код — как мечта идиота. Он никогда не исполнится.
    print("А вот и нет, сука")
}

А теперь безопасный способ, когда всё ок: Если ты позовёшь главную очередь синхронно из другого потока (ну, с какой-нибудь фоновой, global()), то никакого дедлока не будет. Фоновый поток просто присядет на корточки и подождёт, пока главный сделает свои дела.

Пример, как можно (и иногда нужно):

DispatchQueue.global().async {
    // Мы тут на фоне, в безопасности
    // Зовём главную очередь синхронно — МОЖНО!
    DispatchQueue.main.sync {
        // Тут обновляем интерфейс, и всё чётко.
        self.button.title = "Всё ок, ёпта"
    }
    // А после того как главный поток отработал, мы тут и продолжим.
}

Итоговое правило, которое надо выжечь на подкорке: Никогда, блядь, НИКОГДА не вызывай DispatchQueue.main.sync из кода, который может выполняться на главном потоке. А то будет как с Герасимом и Муму — сам утопишь, а потом метаться будешь.