Как происходит переключение контекста между задачами в structured concurrency Swift?

«Как происходит переключение контекста между задачами в structured concurrency Swift?» — вопрос из категории Многопоточность, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В structured concurrency Swift переключение между задачами управляется кооперативной (cooperative) моделью многозадачности на уровне потоков. Система решает, когда приостановить (suspend) одну задачу и возобновить (resume) другую, основываясь на приоритетах и доступности потоков.

Ключевой принцип: Переключение происходит только в точках await.

func fetchUserData() async throws -> User {
    // 1. Задача выполняется на текущем потоке.
    let userRequest = URLRequest(url: userURL)

    // 2. При достижении `await` задача может быть ПРИОСТАНОВЛЕНА.
    //    Система освобождает текущий поток для других задач.
    let (userData, _) = try await URLSession.shared.data(for: userRequest)

    // 3. Когда данные загружены, система ВОЗОБНОВЛЯЕТ эту задачу.
    //    Это может произойти на ДРУГОМ потоке из пула.
    return try JSONDecoder().decode(User.self, from: userData)
}

func fetchUserAndPosts() async throws {
    // Запускаем две дочерние задачи
    async let user = fetchUserData()
    async let posts = fetchPostsData()

    // Ожидаем (`await`) результаты обеих задач.
    // Пока они выполняются (ожидают сетевого ответа), текущая задача приостановлена,
    // а её поток может выполнять другие задачи.
    let (fetchedUser, fetchedPosts) = try await (user, posts)
    print(fetchedUser, fetchedPosts)
}

Как это работает под капотом:

  1. Пул потоков: Swift runtime использует пул рабочих потоков, оптимизированный под количество CPU-ядер.
  2. Приостановка (suspend): При встрече await, если результат не готов немедленно (например, ожидание сети или сна), задача отмечается как приостановленная и освобождает занимаемый поток.
  3. Возобновление (resume): Когда ожидаемое значение становится доступным, система планирует возобновление задачи. Она будет выполнена на первом доступном потоке из пула (не обязательно на том же).
  4. Приоритеты: Задачи наследуют приоритет от родителя (.userInitiated, .utility и т.д.), что влияет на порядок планирования.
  5. Отмена: Отмена родительской задачи автоматически распространяется на все дочерние.

Важное следствие: Поскольку переключение происходит только в await, длительные синхронные вычисления внутри задачи могут блокировать поток. Для CPU-интенсивных операций используйте Task.detached или явно отпускайте поток с помощью await Task.yield().