Irbisa · Шпаргалка

Swift Concurrency

async/await, Task, акторы, structured concurrency, AsyncSequence

Middle-разработчик · 25 вопросов

  1. 01

    Чем конкурентность отличается от параллельности, а чем синхронное выполнение отличается от асинхронного?

    Конкурентность означает, что несколько задач находятся в работе одновременно и могут чередоваться; параллельность означает, что они буквально выполняются в один момент на разных ядрах.

  2. 02

    Что такое GCD, чем отличаются serial- и concurrent-очереди и чем sync отличается от async?

    GCD (Grand Central Dispatch) представляет собой библиотеку Apple: вы отправляете замыкания в очереди, а система выполняет их на своём пуле потоков.

    let serial = DispatchQueue(label: "com.app.cache")   // serial by defaultlet concurrent = DispatchQueue(label: "com.app.io", attributes: .concurrent) serial.async { print("1") }    // returns immediatelyserial.async { print("2") }    // always runs after "1"…
  3. 03

    Как работает async/await в Swift и чем он отличается от completion handler’ов?

    async-функция может приостановиться на каждом await и продолжить позже, а пока она приостановлена, она не держит поток.

    // Before: completion handlerfunc loadUser(id: String, completion: @escaping (Result<User, Error>) -> Void) {  // ...} // After: async/await…
  4. 04

    Как вызвать async-функцию из синхронного кода и можно ли вызывать синхронный код из async-функции?

    Из синхронного кода запускают Task { } и делают await внутри него; синхронная функция возвращается сразу и напрямую результат не получает.

    @MainActor@Observablefinal class ProfileViewModel {  var profile: Profile?  private var loadTask: Task<Void, Never>?…
  5. 05

    Что выведет этот код, если test() вызван на главном потоке, и что изменится, если вызвать его с фоновой очереди?

    func test() {  print("A")  DispatchQueue.main.async { print("B") }  print("C")  DispatchQueue.main.sync { print("D") }  print("E")}test()   // called on the main thread

    На главном потоке выведется A, C, и дальше всё встанет: main.sync ждёт, пока главная очередь выполнит D, а главная очередь занята выполнением самого test(), который ждёт; это deadlock, который libdispatch обычно превращает в краш.

  6. 06

    Какие бывают классы Quality of Service и как они влияют на планирование?

    QoS сообщает системе, насколько работа важна пользователю прямо сейчас, а система по нему выставляет приоритет потока, делит процессорное время и выбирает между производительными и энергоэффективными ядрами.

  7. 07

    Что такое DispatchGroup и когда его использовать?

    DispatchGroup считает набор асинхронных задач и сообщает, когда все они закончились.

    func loadAll(ids: [String], completion: @escaping ([User]) -> Void) {  let group = DispatchGroup()  let lock = NSLock()  var users: [User] = []   for id in ids {…
  8. 08

    Чем GCD отличается от OperationQueue и когда стоит выбрать OperationQueue?

    OperationQueue служит надстройкой над GCD: она оборачивает работу в объекты Operation и добавляет зависимости, лимит конкурентности, отмену и наблюдаемое состояние; GCD просто выполняет замыкания.

  9. 09

    Чем race condition отличается от data race и от deadlock и как предотвратить каждую из проблем?

    Гонка данных (data race) возникает, когда два потока одновременно обращаются к одной памяти, хотя бы один пишет, а синхронизации нет, и это неопределённое поведение.

  10. 10

    Что такое семафор, чем он отличается от мьютекса и когда DispatchSemaphore становится плохой идеей?

    Семафор работает как счётчик, который пропускает до N потоков и блокирует остальные; мьютекс пускает в критическую секцию ровно один поток и имеет владельца: разблокировать его может только поток, который его захватил.

    import Synchronization // Mutex: one thread at a time, and the owner unlocksfinal class Counter: Sendable {  private let value = Mutex(0)  func increment() { value.withLock { $0 += 1 } }…
  11. 11

    Что такое structured concurrency и когда использовать async let, а когда task group?

    Structured concurrency привязывает дочерние задачи к области видимости, которая их создала: пережить её они не могут, отмена идёт вниз, ошибки поднимаются вверх, а родитель ждёт все дочерние задачи.

    // Fixed fan-out: three different requests in parallelfunc loadDashboard() async throws -> Dashboard {  async let user = api.user()  async let posts = api.posts()   // all three start right here  async let news = api.news()  return try await Dashboard(user: user, posts: posts, news: news)…
  12. 12

    Чем Task { } отличается от Task.detached и как определяется приоритет задачи?

    Task { } наследует изоляцию актора, приоритет и task-local значения кода, который его создаёт; Task.detached не наследует ничего из этого и выполняется nonisolated со своим приоритетом.

    @MainActor@Observablefinal class FeedModel {  var items: [Item] = []   func refresh() {…
  13. 13

    Как работает отмена задач в Swift Concurrency?

    Отмена кооперативная: cancel() лишь выставляет флаг у задачи и всех её дочерних задач, и ничего не остановится, пока код не проверит этот флаг или не сделает await на API, которое проверит его само.

    func processAll(_ items: [Item]) async throws {  for item in items {    try Task.checkCancellation()        // stop between units of work    try await process(item)  }}…
  14. 14

    Найдите баг: после того как экран вызывает stop(), тикер продолжает дёргать API, а загрузка CPU резко растёт.

    // api: a Sendable global whose fetchPrice() is async throws -> Decimal@MainActorfinal class PriceTicker {  private var task: Task<Void, Never>?  private(set) var price: Decimal = 0   func start() {    task = Task {      while true {        if let p = try? await api.fetchPrice() { price = p }        try? await Task.sleep(for: .seconds(5))      }    }  }  func stop() { task?.cancel() }}

    try? проглатывает CancellationError, который должен остановить цикл.

  15. 15

    loadUser() бросает ошибку через 0,1 с, а loadPosts() идёт 10 с: что будет с запросом постов и когда вернётся loadScreen()?

    func loadScreen() async throws -> Screen {  async let posts = api.loadPosts()   // takes 10 s  async let user = api.loadUser()     // throws after 0.1 s  let u = try await user  let p = try await posts  return Screen(user: u, posts: p)}

    Ошибка всплывает на try await user, и перед выходом из функции Swift неявно отменяет posts и дожидается его, поэтому loadScreen() бросит ошибку только тогда, когда задача постов свернётся: почти сразу, если loadPosts уважает отмену, и через все 10 с, если игнорирует.

  16. 16

    Нужно скачать 500 картинок, но не больше 5 одновременно: как ограничить конкурентность в task group?

    Заполните группу 5 дочерними задачами, а потом каждый раз, когда одна завершается (next() возвращает результат), добавляйте следующую, и в работе никогда не будет больше 5.

    func downloadAll(_ urls: [URL], maxConcurrent: Int = 5) async throws -> [URL: Data] {  try await withThrowingTaskGroup(of: (URL, Data).self) { group in    var pending = urls.makeIterator()    var results: [URL: Data] = [:]     // Seed the first `maxConcurrent` downloads…
  17. 17

    Как добавить таймаут к await-вызову, если у API нет параметра таймаута?

    Устройте гонку операции с Task.sleep в throwing task group: берите ту дочернюю задачу, которая закончится первой, и отменяйте вторую.

    struct TimeoutError: Error {} func withTimeout<T: Sendable>(  _ duration: Duration,  operation: @escaping @Sendable () async throws -> T) async throws -> T {…
  18. 18

    Что такое актор, как он предотвращает гонки данных и почему его методы снаружи вызываются через await?

    Актор представляет собой ссылочный тип, который защищает своё изменяемое состояние: в нём одновременно выполняется только один фрагмент кода, на собственном последовательном executor’е.

    actor BankAccount {  let id: UUID  private(set) var balance: Decimal = 0   init(id: UUID) { self.id = id }…
  19. 19

    Что гарантирует @MainActor, почему не стоит помечать им всё подряд и как увести работу с него?

    @MainActor является глобальным актором, чьим executor’ом служит главный поток: помеченный код гарантированно выполняется там, а вызывающие из других доменов изоляции обязаны делать await.

    @MainActor@Observablefinal class FeedViewModel {  var items: [Item] = []   func reload() async throws {…
  20. 20

    Что такое Sendable и какие типы соответствуют ему автоматически?

    Маркерный протокол Sendable означает, что значение можно безопасно передавать между доменами конкурентности (задачами, акторами) без гонки данных.

    import Synchronization struct User: Sendable {            // all stored properties are Sendable  let id: UUID  var name: String}…
  21. 21

    Как обернуть API на completion handler’ах в async/await и чем checked-continuation отличается от unsafe?

    Вызовите withCheckedThrowingContinuation, запустите внутри legacy-вызов и возобновите continuation ровно один раз из колбэка; ожидающая задача остаётся приостановленной до этого момента.

    // Legacy APIfunc fetchUser(id: String, completion: @escaping (Result<User, Error>) -> Void) {  // ...} // Async wrapper…
  22. 22

    Что такое AsyncSequence и AsyncStream и когда их использовать?

    AsyncSequence описывает последовательность, элементы которой приходят со временем и потребляются через for await; AsyncStream даёт готовую реализацию, которую вы собираете сами, чтобы превратить колбэки, делегаты или нотификации в такую последовательность.

    // LocationTracker: a Sendable wrapper around a delegate-based APIfunc locationUpdates() -> AsyncStream<CLLocation> {  AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in    let tracker = LocationTracker()    tracker.onUpdate = { continuation.yield($0) }    continuation.onTermination = { _ in tracker.stop() }   // cancelled or finished…
  23. 23

    Этот актор всё равно скачивает одну и ту же картинку дважды, когда её одновременно просят два view: почему и как это исправить?

    // downloader: a Sendable global dependency whose fetch(_:) is asyncactor ImageCache {  private var cache: [URL: UIImage] = [:]   func image(for url: URL) async throws -> UIImage {    if let cached = cache[url] { return cached }    let image = try await downloader.fetch(url)   // suspension point    cache[url] = image    return image  }}

    Акторы реентерабельны: на await актор освобождается, выполняется второй вызов, видит, что кэш всё ещё пуст, и запускает свою загрузку.

  24. 24

    Что такое cooperative thread pool и почему его блокировка через DispatchSemaphore.wait() или Thread.sleep считается багом?

    Swift Concurrency выполняет задачи на кооперативном пуле, где примерно по одному потоку на ядро CPU, и рассчитывает, что каждая задача либо выполняется, либо приостанавливается.

    // wrong: called from async code, this blocks a pool thread; one per core and the app is wedgedfunc loadSync() -> Data {  let semaphore = DispatchSemaphore(value: 0)  var result = Data()  Task {    result = (try? await api.download()) ?? Data()…
  25. 25

    Как защитить общее изменяемое состояние (актор, lock или serial-очередь) и как выбрать между ними?

    Актор выбирают, когда вызывающие асинхронные и внутри секции нужен await; lock (Mutex, OSAllocatedUnfairLock) подходит, когда доступ короткий, синхронный и частый; serial-очередь уместна внутри GCD-кода.

    import Synchronization // Hot, synchronous, tiny critical section: a lockfinal class HitCounter: Sendable {  private let counts = Mutex<[String: Int]>([:])  func record(_ key: String) { counts.withLock { $0[key, default: 0] += 1 } }…