GCD 中的并发队列与串行队列

我正在努力完全理解 GCD 中的并发和串行队列。我有一些问题,希望有人可以清楚地回答我,在这一点上。

  1. 我读到,为了一个接一个地执行任务,创建并使用了串行队列。然而,如果:

    • 我创建了一个序列队列
    • 我三次使用 dispatch_async(对于我刚刚创建的串行队列)来分派三个块 A、 B、 C

    这三个街区是否会被执行:

    • 因为队列是串行的

      或者

    • 并发(在并行线程上同时) ,因为我使用 ASYNC 分派
  2. 我读到我可以在并发队列上使用 dispatch_sync来执行一个接一个的块。在这种情况下,为什么串行队列甚至存在,因为我总是可以使用一个并发队列,在那里我可以分派同步,因为我想要多少块?

    谢谢你的解释!

64948 次浏览

一个简单的例子: 有一个块需要一分钟才能执行。将其从主线程添加到队列中。我们来看看这四个案子。

  • 异步-并发: 代码在后台线程上运行。控件立即返回到主线程(和 UI)。该块不能假设它是该队列上运行的唯一块
  • 异步串行: 代码在后台线程上运行。控件立即返回到主线程。块 可以假设它是在该队列上运行的唯一块
  • 同步-并发: 代码在后台线程上运行,但主线程等待它完成,阻止对 UI 的任何更新。该块不能假定它是该队列上运行的唯一块(几秒钟前我可以使用异步添加另一个块)
  • Sync-Series: 代码在后台线程上运行,但主线程等待它完成,阻止对 UI 的任何更新。块 可以假设它是在该队列上运行的唯一块

显然,对于长时间运行的流程,您不会使用后两者中的任何一个。当您试图从可能在另一个线程上运行的内容更新 UI (总是在主线程上)时,通常会看到它。

这里有几个实验,我已经做了,使我了解这些 serialconcurrentGrand Central Dispatch队列。

 func doLongAsyncTaskInSerialQueue() {


let serialQueue = DispatchQueue(label: "com.queue.Serial")
for i in 1...5 {
serialQueue.async {


if Thread.isMainThread{
print("task running in main thread")
}else{
print("task running in background thread")
}
let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
let _ = try! Data(contentsOf: imgURL)
print("\(i) completed downloading")
}
}
}

在 GCD 中使用异步时,Task 将在不同的线程(主线程除外)中运行。异步意味着执行下一行不要等到块执行结果非阻塞主线程和主队列。 由于它的串行队列,所有这些都按照它们添加到串行队列的顺序执行。串行执行的任务总是由与 Queue 关联的单个线程一次执行一个。

func doLongSyncTaskInSerialQueue() {
let serialQueue = DispatchQueue(label: "com.queue.Serial")
for i in 1...5 {
serialQueue.sync {
if Thread.isMainThread{
print("task running in main thread")
}else{
print("task running in background thread")
}
let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
let _ = try! Data(contentsOf: imgURL)
print("\(i) completed downloading")
}
}
}

在 GCD 中使用同步时,任务可能在主线程中运行。Sync 在给定队列上运行一个块,并等待它完成,这将导致阻塞主线程或主队列。由于主队列需要等待直到分派的块完成,因此主线程将可用于处理来自主队列以外的队列的块。因此,在后台队列上执行的代码有可能实际上正在主线程上执行 由于它的串行队列,所有执行的顺序,他们被添加(先进先出)。

func doLongASyncTaskInConcurrentQueue() {
let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
for i in 1...5 {
concurrentQueue.async {
if Thread.isMainThread{
print("task running in main thread")
}else{
print("task running in background thread")
}
let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
let _ = try! Data(contentsOf: imgURL)
print("\(i) completed downloading")
}
print("\(i) executing")
}
}

在 GCD 中使用异步时,Task 将在后台线程中运行。异步意味着执行下一行不要等到块执行结果非阻塞主线程。 请记住,在并发队列中,任务按照添加到队列中的顺序进行处理,但是将不同的线程附加到 请记住,他们不应该按顺序完成任务 任务的顺序每次都不同 线程必须自动创建。任务并行执行 当达到(maxConcurrentOperationCount)时,某些任务将表现出 作为一个串行,直到一个线程是自由的。

func doLongSyncTaskInConcurrentQueue() {
let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
for i in 1...5 {
concurrentQueue.sync {
if Thread.isMainThread{
print("task running in main thread")
}else{
print("task running in background thread")
}
let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
let _ = try! Data(contentsOf: imgURL)
print("\(i) completed downloading")
}
print("\(i) executed")
}
}

在 GCD 中使用同步时,任务可能在主线程中运行。Sync 在给定队列上运行一个块,并等待它完成,这将导致阻塞主线程或主队列。由于主队列需要等待直到分派的块完成,因此主线程将可用于处理来自主队列以外的队列的块。因此,在后台队列上执行的代码有可能实际上正在主线程上执行。 由于其并发队列,任务可能无法按照添加到队列中的顺序完成。但对于同步操作,虽然它们可能由不同的线程处理,但它们确实可以。因此,它的行为就像这是串行队列一样。

下面是这些实验的总结

请记住,使用 GCD 时,您只是将任务添加到 Queue 并从该队列执行任务。队列在主线程或后台线程中分派任务,具体取决于操作是同步的还是异步的。队列的类型有串行、并发、主分派队列。默认情况下,您执行的所有任务都来自 Main 调度队列。已经有四个预定义的全局并发队列供应用程序使用,还有一个主队列(DispatchQueue.main)。还可以手动创建自己的队列并从该队列执行任务。

通过将任务分派到 Main 队列,始终应该从主线程执行与 UI 相关的任务。简单的实用程序是 DispatchQueue.main.sync/async,而网络相关/重型操作应该总是异步完成,不管你使用的是主线程还是后台线程

编辑: 但是,有些情况下,您需要在后台线程中同步执行网络调用操作,而无需冻结 UI (例如,刷新 OAuth 令牌并等待它是否成功)。您需要将该方法封装在异步操作中。这样就可以按顺序执行繁重的操作,而不会阻塞主线程。

func doMultipleSyncTaskWithinAsynchronousOperation() {
let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
concurrentQueue.async {
let concurrentQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default)
for i in 1...5 {
concurrentQueue.sync {
let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
let _ = try! Data(contentsOf: imgURL)
print("\(i) completed downloading")
}
print("\(i) executed")
}
}
}

编辑: 你可以在这里观看演示视频

如果我正确理解 GCD 的工作原理,我认为 DispatchQueue有两种类型,serialconcurrent,同时,DispatchQueue分配任务的方式有两种,分配的 closure,第一种是 async,另一种是 sync。它们一起决定了闭包(任务)实际上是如何执行的。

我发现 serialconcurrent意味着该队列可以使用多少个线程,serial意味着一个,而 concurrent意味着许多个。syncasync意味着任务将在哪个线程上执行,调用者的线程或该队列下面的线程,sync意味着在调用者的线程上运行,而 async意味着在底层线程上运行。

下面是可以在 Xcode 游戏场上运行的实验性代码。

PlaygroundPage.current.needsIndefiniteExecution = true
let cq = DispatchQueue(label: "concurrent.queue", attributes: .concurrent)
let cq2 = DispatchQueue(label: "concurent.queue2", attributes: .concurrent)
let sq = DispatchQueue(label: "serial.queue")


func codeFragment() {
print("code Fragment begin")
print("Task Thread:\(Thread.current.description)")
let imgURL = URL(string: "http://stackoverflow.com/questions/24058336/how-do-i-run-asynchronous-callbacks-in-playground")!
let _ = try! Data(contentsOf: imgURL)
print("code Fragment completed")
}


func serialQueueSync() { sq.sync { codeFragment() } }
func serialQueueAsync() { sq.async { codeFragment() } }
func concurrentQueueSync() { cq2.sync { codeFragment() } }
func concurrentQueueAsync() { cq2.async { codeFragment() } }


func tasksExecution() {
(1...5).forEach { (_) in
/// Using an concurrent queue to simulate concurent task executions.
cq.async {
print("Caller Thread:\(Thread.current.description)")
/// Serial Queue Async, tasks run serially, because only one thread that can be used by serial queue, the underlying thread of serial queue.
//serialQueueAsync()
/// Serial Queue Sync, tasks run serially, because only one thread that can be used by serial queue,one by one of the callers' threads.
//serialQueueSync()
/// Concurrent Queue Async, tasks run concurrently, because tasks can run on different underlying threads
//concurrentQueueAsync()
/// Concurrent Queue Sync, tasks run concurrently, because tasks can run on different callers' thread
//concurrentQueueSync()
}
}
}
tasksExecution()

希望能有所帮助。

首先,了解线程和队列之间的区别以及 GCD 的真正作用非常重要。当我们使用分派队列(通过 GCD)时,我们实际上是在排队,而不是线程化。Dispatch 框架的设计是为了让我们远离线程,因为苹果承认“实现一个正确的线程解决方案(可能)变得极其困难,如果不是(有时)不可能实现的话。”因此,要同时执行任务(我们不希望冻结 UI 的任务) ,我们所需要做的就是创建这些任务的队列并将其交给 GCD。GCD 处理所有相关的线程。因此,我们真正要做的就是排队。

第二件需要马上知道的事情是任务是什么。任务是该队列块中的所有代码(不在队列中,因为我们可以随时向队列中添加内容,而是在将其添加到队列的闭包中)。一个任务有时被称为块,一个块有时被称为任务(但它们更常被称为任务,特别是在 Swift 社区中)。不管有多少代码,大括号中的所有代码都被认为是一个单独的任务:

serialQueue.async {
// this is one task
// it can be any number of lines with any number of methods
}
serialQueue.async {
// this is another task added to the same queue
// this queue now has two tasks
}

很明显,并发意味着与其他事物在同一时间发生,而串行意味着一个接一个(从不在同一时间发生)。序列化,或者说串行化,就是从头到尾按照从左到右、从上到下、不间断的顺序来执行。

有两种类型的队列,串行和并发,但所有队列都是相对于彼此并发的。您想要在“后台”中运行任何代码的事实意味着您想要与另一个线程(通常是主线程)并发地运行它。因此,所有的分派队列,无论是串行的还是并发的,都会同时执行它们的任务 相对于其他队列。任何由队列(通过串行队列)执行的序列化只与单个[串行]调度队列中的任务有关(就像上面的例子,在同一个串行队列中有两个任务; 这些任务将一个接一个地执行,永远不会同时执行)。

序列队列 (通常称为私有调度队列)保证从开始到结束,按照将任务添加到特定队列的顺序,每次执行一个任务。这是在讨论分派队列时在任何位置进行序列化的唯一保证——特定串行队列中的特定任务以串行方式执行。但是,如果串行队列是独立的队列,则它们可以与其他串行队列同时运行,因为所有队列都是相对于彼此并发的。所有任务都在不同的线程上运行,但不能保证每个任务都在同一个线程上运行(这不重要,但很有意思)。而且 iOS 框架没有任何现成的串行队列,您必须创建它们。私有(非全局)队列默认是串行的,因此要创建串行队列:

let serialQueue = DispatchQueue(label: "serial")

可以通过其属性属性使其并发:

let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])

但是在这一点上,如果您没有向私有队列添加任何其他属性,Apple 建议您只使用它们的一个随时可用的全局队列(它们都是并发的)。在这个答案的底部,您将看到另一种创建串行队列的方法(使用目标属性) ,这是 Apple 推荐的方法(为了更有效的资源管理)。但就目前而言,给它贴上标签就足够了。

CONCURRENT QUEUES (通常称为全局调度队列)可以同时执行任务; 然而,按照将任务添加到特定队列的顺序,任务保证为 启动,但是与串行队列不同,队列不会等到第一个任务完成后才开始第二个任务。任务(与串行队列一样)运行在不同的线程上,并且(与串行队列一样)不能保证每个任务都在同一个线程上运行(这不重要,但很有意思)。IOS 框架提供了四个即时可用的并发队列。你可以使用上面的例子创建一个并发队列,或者使用苹果的一个全局队列(这通常是推荐的) :

let concurrentQueue = DispatchQueue.global(qos: .default)

保留循环抗性: 分派队列是引用计数的对象,但是您不需要保留和释放全局队列,因为它们 是全局的,因此保留和释放被忽略 直接全局队列,而不必将它们分配给属性

有两种分派队列的方法: 同步和异步。

SYNC DISPATCHING 意味着调度队列的线程(调用线程)在调度队列后暂停,等待队列块中的任务完成执行后再继续。同步发送:

DispatchQueue.global(qos: .default).sync {
// task goes in here
}

ASYNC DISPATCHING 意味着调用线程在分派队列之后继续运行,不等待队列块中的任务完成执行。异步分派:

DispatchQueue.global(qos: .default).async {
// task goes in here
}

现在,人们可能认为,为了执行串行任务,应该使用串行队列,这是不完全正确的。为了串行执行 多个任务,应该使用一个串行队列,但是所有任务(独立的)都是串行执行的。考虑一下这个例子:

whichQueueShouldIUse.syncOrAsync {
for i in 1...10 {
print(i)
}
for i in 1...10 {
print(i + 100)
}
for i in 1...10 {
print(i + 1000)
}
}

无论您如何配置(串行或并发)或分派(同步或异步)此队列,此任务始终以串行方式执行。第三个循环永远不会在第二个循环之前运行,第二个循环永远不会在第一个循环之前运行。在使用任何分派的任何队列中都是如此。当您引入多个任务和/或队列时,串行和并发就会真正发挥作用。

考虑这两个队列,一个串行队列和一个并发队列:

let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue.global(qos: .default)

假设我们在异步中分派两个并发队列:

concurrentQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}


1
101
2
102
103
3
104
4
105
5

它们的输出是混乱的(正如预期的那样) ,但是请注意,每个队列都以串行方式执行自己的任务。这是最基本的并发示例——两个任务同时在同一队列的后台运行。现在让我们制作第一个连载:

serialQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}


101
1
2
102
3
103
4
104
5
105

第一个队列不是应该串行执行吗?是的(第二次也是如此)。后台发生的其他任何事情都与队列无关。我们告诉串行队列以串行方式执行,它确实执行了... 但我们只给了它一个任务。现在让我们给它两个任务:

serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}


1
2
3
4
5
101
102
103
104
105

这是序列化的最基本(也是唯一可能的)示例——在同一个队列中的后台(到主线程)中串行运行的两个任务(一个接一个)。但是,如果我们将它们设置为两个独立的串行队列(因为在上面的示例中它们是相同的队列) ,它们的输出将再次混乱:

serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue2.async {
for i in 1...5 {
print(i + 100)
}
}


1
101
2
102
3
103
4
104
5
105

这就是我所说的所有队列都是相对于彼此并发的。这是两个同时执行其任务的串行队列(因为它们是独立的队列)。队列不知道或不关心其他队列。现在让我们回到两个串行队列(同一个队列) ,并添加第三个队列,一个并发队列:

serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 1000)
}
}


1
2
3
4
5
101
102
103
104
105
1001
1002
1003
1004
1005

这有点出乎意料,为什么并发队列要等到串行队列完成后才执行?这不是并发。你的操场可能显示不同的输出,但我的显示了这一点。之所以会出现这种情况,是因为并发队列的优先级不够高,GCD 无法更快地执行它的任务。因此,如果我保持所有内容不变,但是改变全局队列的 QoS (其服务质量,也就是队列的优先级) let concurrentQueue = DispatchQueue.global(qos: .userInteractive),那么输出就如预期的那样:

1
1001
1002
1003
2
1004
1005
3
4
5
101
102
103
104
105

两个串行队列按照预期的顺序执行任务,并发队列执行任务的速度更快,因为它被赋予了高优先级(高 QoS 或服务质量)。

与我们的第一个打印示例一样,两个并发队列显示一个混乱的打印输出(正如预期的那样)。为了使它们在串行中整齐地打印,我们必须使它们成为同一串行队列 (队列的相同实例,而不仅仅是相同的标签)。然后,每个任务相对于其他任务按顺序执行。然而,让它们串行打印的另一种方法是保持它们并发,但改变它们的分派方法:

concurrentQueue.sync {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}


1
2
3
4
5
101
102
103
104
105

请记住,同步分派仅意味着调用线程等待队列中的任务完成后再继续。这里需要注意的是,在第一个任务完成之前,调用线程将被冻结,这可能是您希望 UI 执行的方式,也可能不是。

正是由于这个原因,我们不能采取以下措施:

DispatchQueue.main.sync { ... }

这是我们不能执行的队列和分派方法的唯一可能组合ーー对主队列进行同步分派。这是因为我们要求主队列冻结,直到我们在花括号内执行任务... 我们分配到主队列,我们刚刚冻结。这就是所谓的死锁。在操场上观看它的运作:

DispatchQueue.main.sync { // stop the main queue and wait for the following to finish
print("hello world") // this will never execute on the main queue because we just stopped it
}
// deadlock

最后要提到的是资源。当我们给一个队列一个任务时,GCD 会从它的内部管理池中找到一个可用的队列。至于这个答案的写作,每个 qos 有64个队列可用。这看起来似乎很多,但它们可以很快被消费掉,特别是第三方库,尤其是数据库框架。基于这个原因,苹果推荐了队列管理(在下面的链接中提到) ,其中一个是:

不要创建私有并发队列,而是将任务提交给 对于串行任务,将 将串行队列的目标设置为全局并发队列之一。 这样,您就可以维护队列的序列化行为,同时 最小化创建线程的单独队列的数量。

为了做到这一点,与其像我们以前那样创建它们(现在仍然可以) ,苹果推荐创建这样的串行队列:

let serialQueue = DispatchQueue(label: "serialQueue", qos: .default, attributes: [], autoreleaseFrequency: .inherit, target: .global(qos: .default))

使用扩展,我们可以得到这样的结果:

extension DispatchQueue {
public class func serial(label: String, qos: DispatchQoS = .default) -> DispatchQueue {
return DispatchQueue(label: label,
qos: qos,
attributes: [],
autoreleaseFrequency: .inherit,
target: .global(qos: qos.qosClass))
}
}


let defaultSerialQueue = DispatchQueue.serial(label: "xyz")
let serialQueue = DispatchQueue.serial(label: "xyz", qos: .userInteractive)


// Which now looks like the global initializer
let concurrentQueue = DispatchQueue.global(qos: .default)

如需进一步阅读,我建议如下:

Https://developer.apple.com/library/archive/documentation/general/conceptual/concurrencyprogrammingguide/introduction/introduction.html#//apple_ref/doc/uid/tp40008091-ch1-sw1

Https://developer.apple.com/documentation/dispatch/dispatchqueue

1.我读到,为了一个接一个地执行任务,创建并使用了串行队列。然而,如果:- •我创建了一个序列队列 •我三次对我刚刚创建的串行队列使用 /////////////////////////////////////////////////////////////////////////////

回答 :- 这三个模块一个接一个地执行。我创建了一个示例代码,有助于理解。

let serialQueue = DispatchQueue(label: "SampleSerialQueue")
//Block first
serialQueue.async {
for i in 1...10{
print("Serial - First operation",i)
}
}


//Block second
serialQueue.async {
for i in 1...10{
print("Serial - Second operation",i)
}
}
//Block Third
serialQueue.async {
for i in 1...10{
print("Serial - Third operation",i)
}
}

我喜欢用这个比喻来思考这个问题(这是原始图像的 链接) :

Dad's gonna need some help

让我们想象一下,你爸爸正在洗碗,而你刚刚喝了一杯苏打水。你把杯子拿给你爸爸清理,把它放在另一个盘子的旁边。

现在你爸爸一个人洗碗,所以他得一个一个洗: 你爸爸代表 串行队列

但你不会真的对站在那里看着它被清理干净感兴趣。所以,你放下玻璃,回到你的房间: 这是所谓的 异步调度。你爸爸可能会,也可能不会让你知道,一旦他完成了,但重要的一点是,你不是在等待玻璃清理; 你回到你的房间,你知道,孩子的东西。

现在让我们假设你仍然口渴,想在你最喜欢的那个杯子上喝点水,而且你真的想在杯子清理干净后马上拿回来。所以你就站在那儿看着你爸洗碗直到你爸洗完。这是一个 同步调度,因为您在等待任务完成时被阻塞了。

最后假设你妈妈决定帮你爸爸一起洗碗。现在队列变成了 并行队列,因为他们可以同时清洗多个盘子; 但是请注意,你仍然可以决定在那里等待或者回到你的房间,不管它们是如何工作的。

希望这个能帮上忙