Irbisa · Шпаргалка

GCD и потоки

Очереди, QoS, DispatchGroup и семафоры, deadlock на main.sync, Operation, связь с async/await

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

  1. 01

    Что такое Thread (NSThread) и почему в iOS-приложениях потоки почти никогда не создают вручную?

    Thread представляет собой обёртку Foundation над POSIX-потоком: вы сами его создаёте, запускаете и отвечаете за его жизнь.

  2. 02

    На каком потоке вызывается completion handler у URLSession и как из него обновить UI?

    На фоновом: сессия вызывает completion handler на своей delegate-очереди, а у URLSession.shared это приватная serial OperationQueue, а не главная очередь.

  3. 03

    Как выполнить код с задержкой через GCD и как отменить его до запуска?

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { … } планирует замыкание, но обычное замыкание отменить нельзя.

    final class SearchViewController: UIViewController {  private var pendingSearch: DispatchWorkItem?   func searchTextChanged(_ text: String) {    pendingSearch?.cancel()                 // drops the previous item if it has not started    let item = DispatchWorkItem { [weak self] in…
  4. 04

    Что такое Thread Sanitizer и чем он отличается от Main Thread Checker?

    Thread Sanitizer (TSan) представляет собой инструмент времени выполнения, который находит гонки данных (data race): два потока обращаются к одной памяти без синхронизации, и хотя бы один из них пишет.

  5. 05

    Что выведет этот код, если run() вызван на главном потоке, и на каком потоке выполнится каждый блок?

    // run() is called on the main thread, for example from viewDidLoadfunc run() {  let serial = DispatchQueue(label: "com.app.serial")  DispatchQueue.main.async { print("A", Thread.isMainThread) }  serial.sync { print("B", Thread.isMainThread) }  DispatchQueue.global().sync { print("C", Thread.isMainThread) }  serial.async { print("D", Thread.isMainThread) }  serial.sync { print("E", Thread.isMainThread) }  print("F")}

    Он выведет B true, C true, D false, E true, F и в самом конце A true.

  6. 06

    Как barrier-блок превращает concurrent-очередь в reader-writer lock и когда флаг barrier ничего не делает?

    Блок, отправленный с .barrier, ждёт, пока завершатся все блоки, отправленные в очередь раньше, выполняется один и только потом пропускает следующие блоки.

    final class SessionStore: @unchecked Sendable {  private var tokens: [String: String] = [:]  // right: a private concurrent queue. On DispatchQueue.global() the barrier flag is ignored  private let queue = DispatchQueue(label: "com.app.sessions", attributes: .concurrent)   func token(for user: String) -> String? {…
  7. 07

    Найдите баг: этот потокобезопасный кэш падает при первом же вызове image(for:orInsert:).

    final class ImageCache: @unchecked Sendable {  private let queue = DispatchQueue(label: "com.app.image-cache")  private var storage: [URL: UIImage] = [:]  func image(for url: URL) -> UIImage? {    queue.sync { storage[url] }  }   func image(for url: URL, orInsert make: () -> UIImage) -> UIImage {    queue.sync {      if let cached = image(for: url) { return cached }      let image = make()      storage[url] = image      return image    }  }}

    image(for:orInsert:) выполняется внутри queue.sync и вызывает image(for:), который делает queue.sync на той же serial-очереди.

  8. 08

    Найдите баг: loadAvatars иногда так и не вызывает completion.

    // api.fetchAvatar(id:completion:) is declared with a @MainActor completion, called on the main queue@MainActorfunc loadAvatars(ids: [String], completion: @escaping @MainActor ([UIImage]) -> Void) {  let group = DispatchGroup()  var images: [UIImage] = []  for id in ids {    group.enter()    api.fetchAvatar(id: id) { result in      guard case .success(let image) = result else { return }      images.append(image)      group.leave()    }  }  group.notify(queue: .main) { completion(images) }}

    На пути ошибки колбэк выходит через guard до group.leave(), поэтому счётчик группы никогда не доходит до нуля и notify не срабатывает.

  9. 09

    Найдите баг: этот таймер повтора запускается из фоновой очереди и никогда не срабатывает.

    final class UploadRetrier: @unchecked Sendable {  private let queue = DispatchQueue(label: "com.app.upload")  private var retryTimer: Timer?   // Called by the upload code when a request fails  func uploadDidFail() {    queue.async {      self.retryTimer?.invalidate()      self.retryTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] _ in        self?.retry()      }    }  }   private func retry() { /* start the upload again */ }}

    Timer.scheduledTimer добавляет таймер в run loop текущего потока, а здесь это рабочий поток GCD, чей run loop никто не запускает.

  10. 10

    Что делает DispatchQueue.concurrentPerform и чем он лучше global().async в цикле для тяжёлых вычислений?

    concurrentPerform(iterations:execute:) выполняет замыкание N раз параллельно примерно на стольких потоках, сколько ядер, и возвращается после завершения всех итераций.

    import Synchronizationimport Dispatch func checksum(_ chunk: ArraySlice<UInt8>) -> Int { chunk.reduce(0) { $0 &+ Int($1) } } func parallelChecksum(of bytes: [UInt8]) -> Int {…
  11. 11

    Какие локи вы используете в Swift (NSLock, NSRecursiveLock, OSAllocatedUnfairLock, Mutex) и почему не голый os_unfair_lock?

    На iOS 18+ берите Mutex, на iOS 16–17 используйте OSAllocatedUnfairLock: оба владеют защищаемым состоянием и очень быстры.

    import Foundationimport os // wrong: Swift does not guarantee a stable address for a stored struct, so &lock may be a// temporary copy; it often works by accidentfinal class UnsafeCounter: @unchecked Sendable {…
  12. 12

    Что из этого потокобезопасно в Swift: static let, lazy var, Array, в который добавляют элементы, счётчик Int?

    Только static let: глобальные переменные и статические хранимые свойства инициализируются лениво ровно один раз, даже если к ним одновременно впервые обращаются много потоков.

  13. 13

    Как выстроить цепочку работы через зависимости Operation и что происходит с зависимой операцией, если её зависимость отменили?

    b.addDependency(a) не даёт b стать готовой, пока не завершится a, даже если они в разных очередях.

    import Foundation final class ReadFileOperation: Operation, @unchecked Sendable {  let url: URL  private(set) var data: Data?  init(url: URL) { self.url = url }…
  14. 14

    Когда в кодовой базе на Swift 6 и async/await GCD всё ещё остаётся правильным инструментом?

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

  15. 15

    Как написать асинхронный подкласс Operation и почему очередь застревает, если isFinished не поддерживает KVO?

    Переопределите start() вместо main(), сами отдавайте isExecuting и isFinished и отправляйте KVO-уведомления для обоих при каждом изменении.

    import Foundation class AsyncOperation: Operation, @unchecked Sendable {  private let lock = NSLock()  private var _executing = false  private var _finished = false…
  16. 16

    С каким QoS выполняется блок, отправленный через async, как GCD решает проблему инверсии приоритетов и когда не может её решить?

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

  17. 17

    Instruments показывает 60+ потоков, потому что у каждого объекта своя serial-очередь. Почему так происходит и как это исправляют target-очереди?

    Независимые serial-очереди могут работать одновременно, каждая на своём потоке, и получить новый поток, даже когда все ядра заняты.

    // One serial queue per subsystem; every per-object queue funnels into itlet databaseQueue = DispatchQueue(label: "com.app.database", qos: .utility) final class Table: @unchecked Sendable {  private let queue: DispatchQueue  private var rows: [String] = []…
  18. 18

    Совпадает ли DispatchQueue.main.async с выполнением на @MainActor, и что меняется при переводе GCD-кода на Swift Concurrency?

    Во время выполнения оба варианта идут на главном потоке: executor главного актора и есть главная dispatch-очередь.

    nonisolated func didFetch(_ name: String, into label: UILabel) {  // A closure passed literally to DispatchQueue.main.async is treated as @MainActor  DispatchQueue.main.async { label.text = name }   // Through a variable it is a plain @Sendable closure, and touching label is diagnosed:  // let queue = DispatchQueue.main; queue.async { label.text = name }…