iOS | GCD in Swift

In this Grand Central Dispatch tutorial, you’ll delve into basic GCD concepts, including:

  • Multithreading
  • Dispatch queues
  • Concurrency

Multi-core devices, on the other hand, execute multiple threads at the same time via parallelism.

GCD is built on top of threads. Under the hood, it manages a shared thread pool. With GCD, you add blocks of code or work items to dispatch queues and GCD decides which thread to execute them on.

You submit units of work to this queue, and GCD executes them in a FIFO order (first in, first out), guaranteeing that the first task submitted is the first one started.

Dispatch queues are thread-safe, meaning you can simultaneously access them from multiple threads.

Concurrent queues allow multiple tasks to run at the same time. The queue guarantees tasks start in the order you add them. Tasks can finish in any order, and you have no knowledge of the time it will take for the next task to start, nor the number of tasks running at any given time.

Queue Types

GCD provides three main types of queues:

  • Main queue: Runs on the main thread and is a serial queue.
  • Global queues: Concurrent queues shared by the whole system. Four such queues exist, each with different priorities: high, default, low and background. The background priority queue has the lowest priority and is throttled in any I/O activity to minimize negative system impact.
  • Custom queues: Queues you create that can be serial or concurrent. Requests in these queues end up in one of the global queues.

QoS

When sending tasks to the global concurrent queues, you don’t specify the priority directly. Instead, you specify a quality of service (QoS) class property. This indicates the task’s importance and guides GCD in determining the priority to give to the task.

The QoS classes are:

  • User-interactive: This represents tasks that must complete immediately to provide a nice user experience. Use it for UI updates, event handling and small workloads that require low latency. The total amount of work done in this class during the execution of your app should be small. This should run on the main thread.
  • User-initiated: The user initiates these asynchronous tasks from the UI. Use them when the user is waiting for immediate results and for tasks required to continue user interaction. They execute in the high-priority global queue.
  • Utility: This represents long-running tasks, typically with a user-visible progress indicator. Use it for computations, I/O, networking, continuous data feeds and similar tasks. This class is designed to be energy efficient. This gets mapped into the low-priority global queue.
  • Background: This represents tasks the user isn’t directly aware of. Use it for prefetching, maintenance and other tasks that don’t require user interaction and aren’t time-sensitive. This gets mapped into the background priority global queue.

In general, you want to use async when you need to perform a network-based or CPU-intensive task in the background without blocking the current thread.

Delaying Tasks Execution

Why not use Timer? You could consider using it if you have repeated tasks that are easier to schedule with Timer. Here are two reasons to stick with dispatch queue’s asyncAfter():

// 1
let delayInSeconds = 2.0

// 2
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { [weak self] in
  guard let self = self else {
    return
  }

  if !PhotoManager.shared.photos.isEmpty {
    self.navigationItem.prompt = nil
  } else {
    self.navigationItem.prompt = "Add photos with faces to Googlyify them!"
  }

  // 3
  self.navigationController?.viewIfLoaded?.setNeedsLayout()
}

  • One is readability. To use Timer, you have to define a method, then create the timer with a selector or invocation to the defined method. With DispatchQueue.main.asyncAfter(), you simply add a closure.
  • Timer is scheduled on run loops, so you’d also have to make sure you scheduled it on the correct run loop — and in some cases for the correct run loop modes. In this regard, working with dispatch queues is easier.

Readers-Writers Problem

The Swift collection types like Array and Dictionary aren’t thread-safe when declared mutable.

private var unsafePhotos: [Photo] = []
  
var photos: [Photo] {
  return unsafePhotos
}

It may look like there’s a lot of copying in your code when passing collections back and forth. Don’t worry about the memory usage implications of this. The Swift collection types are optimized to make copies only when necessary, for instance, when your app modifies an array passed by value for the first time.

The getter for this property is termed a read method, as it’s reading the mutable array. The caller gets a copy of the array and is protected against inappropriately mutating the original array. However, this doesn’t provide any protection against one thread calling the write method addPhoto(_:) while another thread simultaneously calls the getter for the photos property.

GCD provides an elegant solution of creating a read/write lock using dispatch barriers. Dispatch barriers are a group of functions acting as a serial-style bottleneck when working with concurrent queues.

When you submit a DispatchWorkItem to a dispatch queue, you can set flags to indicate that it should be the only item executed on the specified queue for that particular time. This means all items submitted to the queue prior to the dispatch barrier must complete before DispatchWorkItem executes.

Barrier Flow

Notice how in normal operation, the queue acts just like a normal concurrent queue. But when the barrier is executing, it essentially acts as a serial queue. That is, the barrier is the only thing executing. After the barrier finishes, the queue goes back to being a normal concurrent queue.

private let concurrentPhotoQueue =
  DispatchQueue(
    label: "com.raywenderlich.GooglyPuff.photoQueue",
    attributes: .concurrent)

func addPhoto(_ photo: Photo) {
  // 1
  concurrentPhotoQueue.async(flags: .barrier) { [weak self] in
    guard let self = self else {
      return
    }

    // 2
    self.unsafePhotos.append(photo)

    // 3
    DispatchQueue.main.async { [weak self] in
      self?.postContentAddedNotification()
    }
  }
}

To ensure thread safety with your writes, you need to perform reads on concurrentPhotoQueue. You need return data from the function call, so an asynchronous dispatch won’t cut it. In this case, sync would be an excellent candidate.

You need to be careful, though. Imagine if you call sync and target the current queue you’re already running on. This would result in a deadlock situation.

Deadlocks

In your case, the sync call will wait until the closure finishes, but the closure can’t finish — or even start! — until the currently executing closure finishes, which it can’t! This should force you to be conscious of which queue you’re calling from — as well as which queue you’re passing in.

Here’s a quick overview of when and where to use sync:

  • Main queue: Be very careful for the same reasons as above. This situation also has potential for a deadlock condition, which is especially bad on the main queue because the whole app will become unresponsive.
  • Global queue: This is a good candidate to sync work through dispatch barriers or when waiting for a task to complete so you can perform further processing.
  • Custom serial queue: Be very careful in this situation. If you’re running in a queue and call sync targeting the same queue, you’ll definitely create a deadlock.
var photos: [Photo] {
  var photosCopy: [Photo] = []

  // 1
  concurrentPhotoQueue.sync {
    // 2
    photosCopy = self.unsafePhotos
  }
  return photosCopy
}

Using Dispatch Groups

How can you monitor these concurrent asynchronous events to achieve this?

With dispatch groups, you can group together multiple tasks. Then, you can either wait for them to complete or receive a notification once they finish. Tasks can be asynchronous or synchronous and can even run on different queues.

DispatchGroup manages dispatch groups. You’ll first look at its wait method. This synchronous method blocks your current thread until all the group’s enqueued tasks finish.

// 1
DispatchQueue.global(qos: .userInitiated).async {
  var storedError: NSError?

  // 2
  let downloadGroup = DispatchGroup()
  for address in [
    PhotoURLString.overlyAttachedGirlfriend,
    PhotoURLString.successKid,
    PhotoURLString.lotsOfFaces
  ] {
    guard let url = URL(string: address) else { return }

    // 3
    downloadGroup.enter()
    let photo = DownloadPhoto(url: url) { _, error in
      storedError = error

      // 4
      downloadGroup.leave()
    }   
    PhotoManager.shared.addPhoto(photo)
  }   

  // 5      
  downloadGroup.wait()

  // 6
  DispatchQueue.main.async {
    completion?(storedError)
  }   
}

Call enter() to manually notify the group that a task has started. You must balance out the number of enter() calls with the number of leave() calls, or your app will crash.

You can use wait(timeout:) to specify a timeout and bail out on waiting after a specified time.

Dispatch Groups Notify

// 2    
downloadGroup.notify(queue: DispatchQueue.main) {
  completion?(storedError)
}

notify(queue:work:) serves as the asynchronous completion closure. It runs when there are no more items left in the group. You also specify that you want to schedule the completion work to run on the main queue.

let _ = DispatchQueue.global(qos: .userInitiated)
DispatchQueue.concurrentPerform(iterations: addresses.count) { index in
  let address = addresses[index]
  guard let url = URL(string: address) else { return }
  downloadGroup.enter()
  let photo = DownloadPhoto(url: url) { _, error in
    storedError = error
    downloadGroup.leave()
  }
  PhotoManager.shared.addPhoto(photo)
}

This implementation includes a curious line of code: let _ = DispatchQueue.global(qos: .userInitiated). This causes GCD to use a queue with a .userInitiated quality of service for the concurrent calls.

When is it appropriate to use DispatchQueue.concurrentPerform(iterations:execute:)? You can rule out serial queues because there’s no benefit there – you may as well use a normal for loop. It’s a good choice for concurrent queues that contain looping, though, especially if you need to keep track of progress.

Canceling Dispatch Blocks

Be aware that you can only cancel a DispatchWorkItem before it reaches the head of a queue and starts executing.

// 1
addresses += addresses + addresses

// 2
var blocks: [DispatchWorkItem] = []

for index in 0..<addresses.count {
  downloadGroup.enter()

  // 3
  let block = DispatchWorkItem(flags: .inheritQoS) {
    let address = addresses[index]
    guard let url = URL(string: address) else {
      downloadGroup.leave()
      return
    }
    let photo = DownloadPhoto(url: url) { _, error in
      storedError = error
      downloadGroup.leave()
    }
    PhotoManager.shared.addPhoto(photo)
  }
  blocks.append(block)

  // 4
  DispatchQueue.main.async(execute: block)
}

// 5
for block in blocks[3..<blocks.count] {

  // 6
  let cancel = Bool.random()
  if cancel {

    // 7
    block.cancel()

    // 8
    downloadGroup.leave()
  }
}
  • You dispatch the block asynchronously to the main queue. For this example, using the main queue makes it easier to cancel select blocks since it's a serial queue. The code that sets up the dispatch blocks is already executing on the main queue. Thus, you know that the download blocks will execute at some later time.

  • If the random value is true, you cancel the block. This can only cancel blocks that are still in a queue and haven't began executing. You can't cancel a block in the middle of execution.

  • Here, you remember to remove the canceled block from the dispatch group.

Using Semaphores

Take a brief look at how you can use semaphores to test asynchronous code.

let url = try XCTUnwrap(URL(string: urlString))

// 1
let semaphore = DispatchSemaphore(value: 0)
_ = DownloadPhoto(url: url) { _, error in
  if let error = error {
    XCTFail("\(urlString) failed. \(error.localizedDescription)")
  }

  // 2
  semaphore.signal()
}
let timeout = DispatchTime.now() + .seconds(defaultTimeoutLengthInSeconds)

// 3
if semaphore.wait(timeout: timeout) == .timedOut {
  XCTFail("\(urlString) timed out")
} 
  • You create a semaphore and set its start value. This represents the number of things that can access the semaphore without needing to increment it. Another name for incrementing a semaphore is signaling it.
  • You signal the semaphore in the completion closure. This increments its count and signals that the semaphore is available to other resources.
  • You wait on the semaphore with a given timeout. This call blocks the current thread until you signal the semaphore. A non-zero return code from this function means that the timeout period expired. In this case, the test fails because the network should not take more than 10 seconds to return — a fair point!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容