Привязывайте платформенное решение к lifecycle, состоянию UI, обратной связи, доступности и цене поддержки для команды.
Вопросы и ответы
11 подробных ответов
Проверка по первоисточникам опубликована для 3 из 11 ответов. Дата и область проверки указаны у каждого проверенного ответа.
01Чем serial очередь отличается от concurrent, что такое main и global — и почему очередь это не поток?
junior
Короткий ответ: Очередь — абстракция «список задач», а не поток: GCD мультиплексирует очереди по общему пулу потоков. Serial выполняет задачи строго по одной в порядке FIFO, concurrent запускает несколько параллельно. Main — единственная очередь с контрактом «выполняюсь на мейн-треде»; global — системные concurrent очереди, по одной на класс QoS.
Подробно:
| Очередь | Тип | Особенность |
|---|---|---|
DispatchQueue.main |
serial | привязана к мейн-треду; весь UI |
DispatchQueue.global(qos:) |
concurrent | системные, общие на приложение |
DispatchQueue(label:) |
serial по умолчанию | своя; attributes: .concurrent — параллельная |
| QoS | Для чего |
|---|---|
.userInteractive |
анимации, реакция на жест — «прямо сейчас» |
.userInitiated |
пользователь ждёт результат (открытие экрана) |
.utility |
долгая работа с прогрессом (загрузка файла) |
.background |
невидимая работа: синк, индексация |
- Очередь ≠ поток — serial очередь не владеет потоком: её задачи могут выполняться на разных потоках пула, но никогда одновременно.
- QoS решает планирование — системе QoS говорит, сколько CPU и энергии дать:
.userInteractive— вне очереди,.backgroundможет подождать.
⚠️ Частая ошибка: «создал очередь — создал поток». Нет: тысячи очередей обслуживаются десятком потоков пула. А вот заблокировать много потоков через sync-ожидания — путь к thread explosion.
02Что произойдёт при вызове DispatchQueue.main.sync с мейн-треда?
junior
Короткий ответ: Дедлок и краш. sync блокирует текущий поток до выполнения блока, а блок ждёт своей очереди на main — serial очереди, чей единственный поток мы только что заблокировали. Никто никого не дождётся.
Подробно:
// уже выполняемся на мейн-треде:
DispatchQueue.main.sync {
print("сюда не дойдём")
}
// 💥 EXC_BAD_INSTRUCTION — дедлок
- Механика —
sync= «поставь блок в очередь и жди завершения». Main — serial: следующий блок начнётся только после текущего. Но текущий (наш код) заблокирован ожиданием → взаимное ожидание навсегда. - С фонового потока — легально —
DispatchQueue.main.sync { ... }из global очереди корректен: блокируется фоновый поток, мейн свободен и выполнит блок. Так иногда синхронно читают UI-состояние, хотяasyncпочти всегда уместнее. - Диагностика вместо догадок —
dispatchPrecondition(condition: .onQueue(.main))проверяет очередь явно, без «sync на всякий случай».
⚠️ Частая ошибка: хелпер «если не мейн — sync на мейн, иначе выполнить сразу», который однажды вызывают из кода, уже стоящего в sync-цепочке. Правило общее: sync на текущую serial очередь — всегда дедлок, main — лишь самый частый случай.
03Что напечатает этот код: async и sync на main и global очередях?
middle
Короткий ответ: 1 3 5 6 2 4 7. Всё решают два правила: sync выполняет блок немедленно, блокируя текущий поток, а main.async лишь ставит блок в очередь — он не начнётся, пока текущий код на мейне не завершится.
Подробно:
// выполняется на мейн-треде
print("1")
DispatchQueue.main.async { print("2") }
DispatchQueue.global().sync {
print("3")
DispatchQueue.main.async { print("4") }
print("5")
}
print("6")
DispatchQueue.main.async { print("7") }
1— обычный синхронный вызов.main.async { 2 }— блок встаёт в очередь main и ждёт: у мейна сейчас выполняется текущий код.global().sync— мейн блокируется, блок выполняется сразу (как правило прямо на мейн-треде: GCD не переключает поток без необходимости). Печатается3,main.async { 4 }— снова только в очередь, затем5.6— продолжение после возврата из sync.main.async { 7 }— третий блок в очередь main.- Текущий код завершился — main разбирает накопленное в порядке FIFO:
2,4,7.
⚠️ Частая ошибка: ответить «2 сразу после 1». async на ту же очередь никогда не выполняется немедленно, даже если очередь «свободна»: сначала обязан завершиться текущий блок на ней.
04Четыре потока инкрементируют общий счётчик по 100 раз, а печатается меньше 400. Почему, и как это чинится?
middle
Короткий ответ: counter += 1 — это чтение, инкремент и запись: три шага без атомарности. Потоки читают одно и то же старое значение и затирают записи друг друга — классическая гонка (data race). Чинится сериализацией доступа: serial очередь, барьер, лок или актор.
Подробно:
var counter = 0
DispatchQueue.concurrentPerform(iterations: 4) { _ in
for _ in 0..<100 { counter += 1 } // read-modify-write — не атомарно
}
print(counter) // например, 317 — и каждый запуск по-разному
Способы починить:
- Serial очередь — весь доступ через
queue.sync { counter += 1 }. - Concurrent + барьер — чтения параллельно, запись эксклюзивно:
@propertyWrapper final class Atomic<Value> {
private let queue = DispatchQueue(label: "atomic", attributes: .concurrent)
private var value: Value
init(wrappedValue: Value) { value = wrappedValue }
var wrappedValue: Value {
get { queue.sync { value } }
set { queue.async(flags: .barrier) { self.value = newValue } }
}
}
- NSLock / os_unfair_lock — минимум накладных расходов.
- Actor — решение уровня языка: компилятор сам не пустит к состоянию мимо изоляции.
Ловить гонки — Thread Sanitizer (схема → Diagnostics → TSan).
⚠️ Частая ошибка: верить, что такой @Atomic чинит counter += 1. Это отдельный get и отдельный set — между ними вклинится другой поток. Атомарной должна быть вся операция read-modify-write, а не свойство по кусочкам.
05Как дождаться завершения N асинхронных операций: DispatchGroup и семафоры?
middle
Короткий ответ: DispatchGroup: перед запуском операции — enter(), в completion — leave(), результат забираем через notify(queue:) без блокировки. DispatchSemaphore(value: N) — чтобы ограничить число одновременных операций. Блокирующий wait() на мейне — замороженный UI или дедлок.
Подробно:
let group = DispatchGroup()
for url in urls {
group.enter()
load(url) { result in
defer { group.leave() } // leave гарантирован на всех путях выхода
store(result)
}
}
group.notify(queue: .main) { updateUI() } // не блокирует
// семафор: не больше 3 загрузок одновременно
let sem = DispatchSemaphore(value: 3)
for url in urls {
worker.async {
sem.wait()
defer { sem.signal() }
download(url)
}
}
- Симметрия enter/leave — ровно один
leave()на каждыйenter();deferспасает от раннего return в completion. Лишний leave — краш, недостающий —notifyне случится никогда. - notify против wait —
notifyпланирует блок и возвращается сразу;wait()останавливает текущий поток (терпимо на фоновом, лучше с таймаутом).
⚠️ Частая ошибка: group.wait() или sem.wait() на мейне «чтобы дождаться сети». Если completion тоже приходит на main — дедлок; если нет — просто фриз UI на всё время ожидания. На мейне — только notify.
06GCD против OperationQueue: когда Operation стоит своих накладных расходов?
middle
Короткий ответ: OperationQueue — надстройка над GCD, превращающая задачу в объект: отмена, зависимости, наблюдаемые состояния, лимит параллелизма. Берите её, когда задачами нужно управлять после запуска; для fire-and-forget достаточно GCD.
Подробно:
| GCD | OperationQueue | |
|---|---|---|
| Отмена | DispatchWorkItem.cancel() — лишь флаг до старта |
cancel() / cancelAllOperations() + проверка isCancelled |
| Зависимости | вручную: group, семафоры | addDependency(_:), даже между разными очередями |
| Состояния | ✗ | KVO: isReady → isExecuting → isFinished |
| Лимит параллелизма | напрямую нет | maxConcurrentOperationCount |
| Приоритеты | QoS очереди/блока | qualityOfService + queuePriority |
| Переиспользование | замыкания | подклассы Operation, тестируемые отдельно |
- Отмена везде кооперативна — и в Operation длинный цикл обязан сам периодически проверять
isCancelledи выходить. - Классический follow-up — асинхронная Operation (обёртка над сетевым вызовом) требует ручного конечного автомата: переопределить
isAsynchronous, самому слать KVO-уведомления дляisExecuting/isFinished— иначе очередь сочтёт операцию завершённой сразу послеmain().
⚠️ Частая ошибка: сравнивать их по скорости. Внутри OperationQueue — тот же GCD; вопрос не «что быстрее», а нужен ли задаче жизненный цикл: отмена, зависимости, наблюдаемость.
07Когда ошибка дочерней задачи отменяет соседние задачи в Swift?
middle
Короткий ответ: В withThrowingTaskGroup сама ошибка дочерней задачи не отменяет соседей. Ошибка наблюдается через next() или обход результатов. Если она выходит из тела группы наружу, группа отменяет остальных детей и ждёт их завершения перед возвратом. Отмена кооперативна.
Подробно:
struct Failure: Error {}
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { throw Failure() }
// Здесь ошибка ребёнка наблюдается и выходит из тела группы.
try await group.next()
}
Если тело перехватит ошибку и продолжит работу, соседние задачи не отменятся автоматически. Если результат упавшего ребёнка вообще не прочитать, группа может завершиться без выбрасывания его ошибки. Вызывайте group.cancelAll(), если ваша политика требует отмены оставшейся работы.
Структурность отдельно гарантирует время жизни: группа не вернётся, пока её дети выполняются. Отмена родителя распространяется вниз по дереву, но дети должны сотрудничать — например, вызывать Task.checkCancellation() или использовать точки приостановки с поддержкой отмены.
У async let ошибка наблюдается при ожидании значения с try. При выходе из области оставшиеся дети отменяются и ожидаются; ненаблюдённая ошибка ребёнка сама по себе не выбрасывается из родителя.
Важно: Отмена не означает принудительное завершение. Ребёнок, игнорирующий отмену, может задержать возврат всей группы.
08Как вынести тяжёлую async-работу с MainActor в Swift 6.2?
senior
Короткий ответ: Пометьте async-функцию @concurrent, чтобы явно выполнять её вне актора вызывающего кода. В Swift 6.2 с настройкой NonisolatedNonsendingByDefault обычная nonisolated async-функция остаётся на акторе вызывающего кода и не гарантирует вынос тяжёлого парсинга с MainActor.
Подробно:
import Foundation
struct Item: Decodable, Sendable { let id: Int }
@concurrent
nonisolated func parseOffMain(_ data: Data) async throws -> [Item] {
try JSONDecoder().decode([Item].self, from: data)
}
@MainActor
func loadItems(_ data: Data) async throws -> [Item] {
try await parseOffMain(data)
}
Нужен Swift 6.2+. @concurrent выбирает generic executor, а не конкретный поток. Вызывающий код ожидает результат, не выполняя синхронный парсинг на своём акторе. Значения, пересекающие границы изоляции, должны проходить применимые concurrency-проверки.
При прежнем поведении Swift, когда NonisolatedNonsendingByDefault выключен, nonisolated async переключается на generic executor. При включённой настройке для явного переключения используйте @concurrent; отдельный актор подходит работнику с собственным изменяемым состоянием.
Task {}, созданный в MainActor-изолированном коде, наследует эту изоляцию. Обёртка вокруг синхронного парсинга сама по себе не переносит работу с MainActor. Task.detached неструктурирован и требует явного управления отменой и временем жизни; это не универсальное исправление.
Источник: Swift SE-0461: изоляция async-функций.
09Что изолируют акторы, что такое reentrancy и зачем нужен MainActor?
middle
Короткий ответ: Актор сериализует доступ к своему мутабельному состоянию: снаружи обращения идут через await, одновременно выполняется максимум один метод. Но акторы реентерабельны: на каждом await внутри метода актор может принять другой вызов — состояние после await нужно перепроверять. @MainActor — глобальный актор поверх мейн-треда, дом для UI-состояния.
Подробно:
actor ImageCache {
private var cache: [URL: Image] = [:]
func image(for url: URL) async throws -> Image {
if let cached = cache[url] { return cached }
// точка приостановки: пока качаем, актор выполняет другие вызовы
let image = try await download(url)
// сюда могли прийти раньше нас — без перепроверки скачаем дважды
cache[url] = image
return image
}
}
- Изоляция — гонки на данных актора исключены компилятором, а не дисциплиной команды.
- Reentrancy — защита от дедлоков ценой интерливинга: инварианты, прочитанные до
await, после него не гарантированы. Классическое лечение дубль-загрузки — словарь in-flightTask-ов: повторный вызов ждёт уже запущенную задачу. nonisolated— для членов, не трогающих состояние (чистые вычисления,Hashable): вызываются безawait.
⚠️ Частая ошибка: «актор — это мьютекс вокруг всего метода». Нет: эксклюзив держится только между точками приостановки. Критическая секция актора заканчивается на первом же await.
10Что на самом деле проверяет компилятор про Sendable в строгом режиме Swift 6?
senior
Короткий ответ: Sendable означает, что значение допускает безопасное использование между доменами изоляции. Swift 6 проверяет такие переходы и изоляцию изменяемого состояния. Не каждый перенос не-Sendable значения запрещён: анализ регионов изоляции может доказать безопасную передачу, если исходный домен больше не обращается к значению.
Подробно:
struct User: Sendable { let id: Int }
final class Token: Sendable {
let value: String
init(value: String) { self.value = value }
}
final class Packet { var value = 0 }
actor Inbox { func accept(_ packet: Packet) {} }
func transfer(to inbox: Inbox) async {
let packet = Packet()
await inbox.accept(packet) // безопасная передача отдельного региона
// Не используем packet повторно в исходном домене.
}
- Проверяемое соответствие — у
struct/enumхранимые значения должны бытьSendable. Неявное соответствие выводится лишь в предусмотренных языком случаях; публичному API часто требуется явное объявление. Обычныйfinalкласс с неизменяемымиSendable-полями может явно объявитьSendable, но не получает его автоматически только из-заlet. - Изоляция — акторы и типы с глобальным актором имеют особые гарантии.
@MainActorозначает изоляцию на главном акторе, а не разрешение обращаться к состоянию отовсюду. - Передача владения — анализ регионов и параметр
sendingпозволяют безопасные передачи некоторых не-Sendableзначений. Нельзя оставлять конфликтующие обращения в исходном домене. @unchecked Sendable— проверку корректности внутренней синхронизации берёт на себя автор; сама аннотация не делает доступ безопасным.
⚠️ Частая ошибка: устранять предупреждение одной аннотацией, не разбираясь, какие задачи разделяют состояние. Сначала определите изоляцию и владение, затем выбирайте актор, передачу значения или корректную синхронизацию.
11Почему Timer не срабатывает на фоновом потоке, и при чём здесь RunLoop?
senior
Короткий ответ: Timer — источник событий RunLoop, а не самостоятельный механизм: чтобы он сработал, на потоке должен крутиться RunLoop. У мейн-треда он запущен всегда; потоки GCD-пула его не крутят — таймер молча не сработает никогда.
Подробно:
// ❌ молча не работает: на потоке пула RunLoop не запущен
DispatchQueue.global().async {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
print("tick") // никогда
}
}
// ✓ без RunLoop: DispatchSourceTimer живёт на очереди
let timer = DispatchSource.makeTimerSource(queue: .global())
timer.schedule(deadline: .now() + 1, repeating: 1)
timer.setEventHandler { print("tick") }
timer.resume() // и держите сильную ссылку на timer
- RunLoop — цикл «жди событие → обработай → спи», обслуживающий таймеры, perform-селекторы и часть делегатов старых API (
NSStream, legacyURLConnection). - Современные замены —
DispatchSourceTimer,Task.sleepв цикле,AsyncTimerSequence— им RunLoop не нужен. - Второй классический вопрос — режимы —
scheduledTimerвешает таймер в.defaultmode, а во время скролла мейновский RunLoop уходит в.tracking: таймер замирает. Лечение:RunLoop.main.add(timer, forMode: .common).
⚠️ Частая ошибка: «починить» фоновый таймер вызовом RunLoop.current.run() — и навсегда занять поток циклом без условия выхода. Если работа периодическая — DispatchSourceTimer проще и безопаснее.
Источники
Источники и редакционная политика
Ссылки на источники помогают изучить тему глубже. Если для ответа опубликована техническая проверка, рядом указаны её дата и область. Мы не связаны с упомянутыми работодателями и не продаём места в подборках. Источники, функции сервисов и формат найма могут меняться.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.