GCD 中的并发队列与串行队列
Posted
技术标签:
【中文标题】GCD 中的并发队列与串行队列【英文标题】:Concurrent vs serial queues in GCD 【发布时间】:2013-10-11 08:12:14 【问题描述】:我很难完全理解 GCD 中的并发和串行队列。我有一些问题,希望有人能清楚地回答我。
我了解到串行队列的创建和使用是为了一个接一个地执行任务。但是,如果发生以下情况会发生什么:
我创建了一个串行队列 我使用dispatch_async
(在我刚刚创建的串行队列上)3 次调度三个块 A、B、C
这三个区块会被执行吗:
因为队列是串行的,所以按 A、B、C 的顺序排列
或
并发(在并行线程上同时)因为我使用了 ASYNC 调度我读到我可以在并发队列上使用dispatch_sync
,以便一个接一个地执行块。在那种情况下,为什么串行队列甚至存在,因为我总是可以使用并发队列,在那里我可以同步调度任意数量的块?
感谢任何好的解释!
【问题讨论】:
一个简单的好前置问题dispatch sync vs async 【参考方案1】:一个简单的例子:你有一个需要一分钟才能执行的块。您将其从主线程添加到队列中。我们来看看这四种情况。
异步 - 并发:代码在后台线程上运行。控制立即返回到主线程(和 UI)。该块不能假定它是该队列上唯一运行的块 异步 - 串行:代码在后台线程上运行。控制立即返回到主线程。该块可以假设它是该队列上唯一运行的块 同步 - 并发:代码在后台线程上运行,但主线程等待它完成,从而阻止对 UI 的任何更新。该块不能假设它是该队列上唯一运行的块(我本可以在几秒钟前使用异步添加另一个块) sync - 串行:代码在后台线程上运行,但主线程等待它完成,从而阻止对 UI 的任何更新。该块可以假设它是该队列上唯一运行的块显然,您不会将最后两个中的任何一个用于长时间运行的进程。当您尝试从可能在另一个线程上运行的东西更新 UI(始终在主线程上)时,通常会看到它。
【讨论】:
所以您告诉我:(1)队列的类型(conc 或 serial)是决定任务是按顺序执行还是并行执行的唯一元素;; (2)调度类型(同步或异步)只是说执行是否进行或不进行下一条指令?我的意思是,如果我调度一个任务 SYNC,代码将阻塞直到该任务完成,无论它在哪个队列上执行? @BogdanAlexandru 正确。队列决定了执行策略,而不是你如何对块进行排队。同步等待块完成,异步没有。 @swiftBUTCHER 在某种程度上,是的。创建队列时,您可以指定最大线程数。如果您添加的任务少于他们将并行执行的任务。除此之外,一些任务将保留在队列中,直到有可用容量。 @PabloA.,主线程是一个串行队列,所以实际上只有两种情况。除此之外,它完全一样。异步立即返回(并且该块可能在当前运行循环结束时执行)。主要问题是如果您将 from 主线程 to 同步到主线程,在这种情况下您会遇到死锁。 @ShauketSheikh 不。主线程是串行队列,但并非所有串行队列都是主线程。在第四点,主线程会阻塞,等待另一个线程来完成它的工作。如果串行队列是主线程,你会遇到死锁。【参考方案2】:这里有几个实验让我了解serial
、concurrent
和Grand 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 中使用异步时,任务将在不同的线程(主线程除外)中运行。异步意味着执行下一行不要等到块执行,这会导致非阻塞主线程和主队列。 由于它的串行队列,所有任务都按照它们添加到串行队列的顺序执行。串行执行的任务总是由与队列关联的单个线程一次执行一个。
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 中使用同步时,任务可能会在主线程中运行。同步在给定队列上运行一个块并等待它完成,这会导致阻塞主线程或主队列。由于主队列需要等待直到调度的块完成,主线程将可用于处理队列以外的队列中的块主队列。因此,后台队列上执行的代码有可能实际上是在主线程上执行的 由于它的串行队列,所有都按照它们添加的顺序执行(FIFO)。
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 中使用异步时,任务将在后台线程中运行。异步意味着执行下一行不要等到块执行导致非阻塞主线程。 请记住,在并发队列中,任务按照它们添加到队列的顺序进行处理,但不同的线程附加到 队列。请记住,他们不应该按照命令完成任务 它们被添加到队列中。任务的顺序每次都不同 线程是自动创建的。任务是并行执行的。拥有超过 that(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 中使用同步时,任务可能会在主线程中运行。同步在给定队列上运行一个块并等待它完成,这会导致阻塞主线程或主队列。由于主队列需要等待直到调度的块完成,主线程将可用于处理队列以外的队列中的块主队列。因此,在后台队列上执行的代码有可能实际上是在主线程上执行的。 由于它的并发队列,任务可能不会按照它们添加到队列的顺序完成。但是对于同步操作,尽管它们可能由不同的线程处理,但它确实如此。因此,它的行为就像这是串行队列。
这里是这些实验的总结
请记住,使用 GCD 您只是将任务添加到队列并从该队列执行任务。队列根据操作是同步的还是异步的,在主线程或后台线程中分派您的任务。队列类型有串行、并发、主调度队列。默认情况下,您执行的所有任务都是从主调度队列完成的。已经有四个预定义的全局并发队列供您的应用程序使用,一个主队列(DispatchQueue.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")
EDIT EDIT:您可以观看演示视频here
【讨论】:
伟大的演示...... 下一行不要等到块执行,这会导致非阻塞主线程这就是为什么如果你在后台线程上使用断点它会跳转到
,因为此时它确实没有执行
@那个懒惰的ios Guy 웃 我还是不明白异步并发和异步串行的区别。使用其中任何一个都有什么含义。它们都在后台运行,不会干扰 UI。为什么你会使用同步?不是所有的代码都同步。一个接一个?
@GitSyncApp 你可以看视频here
@ 那个懒惰的 iOS Guy 웃:谢谢你做那个。我在 slack swift-lang 上发帖。如果你也可以制作一个关于 DispatchGroup 和 DispatchWorkItem 的内容,那就是?。 :D
我测试了你的最后一个,doLongSyncTaskInConcurrentQueue()
函数的concurrentQueue.sync
,它打印主线程,Task will run in different thread
似乎不是真的。【参考方案3】:
首先,重要的是要了解线程和队列之间的区别以及 GCD 的真正作用。当我们使用调度队列(通过 GCD)时,我们实际上是在排队,而不是线程。 Dispatch 框架是专门为让我们远离线程而设计的,因为 Apple 承认“实施正确的线程解决方案 [可能] 变得极其困难,如果不是 [有时] 不可能实现的话。”因此,要同时执行任务(我们不想冻结 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
很明显,并发只是意味着与其他事物同时发生,而串行意味着一个接一个(从不同时)。序列化,或者把东西序列化,就是从头到尾按照从左到右,从上到下,不间断的顺序执行。
有两种类型的队列,串行和并发,但所有队列都是相对于彼此并发的。您想要“在后台”运行任何代码这一事实意味着您想要与另一个线程(通常是主线程)同时运行它。因此,所有调度队列,无论是串行的还是并发的,都相对于其他队列并发地执行它们的任务。任何由队列(串行队列)执行的序列化,只与单个 [串行] 调度队列中的任务有关(如上面的示例中,同一个串行队列中有两个任务;这些任务将在一个之后执行另一个,从不同时)。
SERIAL QUEUES(通常称为私有调度队列)保证任务从开始到结束按添加到特定队列的顺序一次一个地执行。 这是在任何地方讨论调度队列的唯一保证序列化——特定串行队列中的特定任务是串行执行的。但是,如果串行队列是单独的队列,则串行队列可以与其他串行队列同时运行,因为所有队列相对于彼此都是并发的。所有任务都在不同的线程上运行,但并非每个任务都保证在同一个线程上运行(不重要,但很有趣)。而且iOS框架没有自带任何现成的串行队列,你必须自己制作。私有(非全局)队列默认是串行的,所以要创建一个串行队列:
let serialQueue = DispatchQueue(label: "serial")
你可以通过它的attribute属性让它并发:
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])
但此时,如果您不向私有队列添加任何其他属性,Apple 建议您只使用其中一个随时可用的全局队列(它们都是并发的)。在这个答案的底部,您将看到另一种创建串行队列的方法(使用目标属性),这是 Apple 推荐的方式(为了更有效的资源管理)。但是现在,给它贴上标签就足够了。
CONCURRENT QUEUES(通常称为全局调度队列)可以同时执行任务;然而,任务保证按照它们被添加到特定队列的顺序启动,但与串行队列不同,队列不会在开始第二个任务之前等待第一个任务完成。任务(与串行队列一样)在不同的线程上运行,并且(与串行队列一样)并非每个任务都保证在同一个线程上运行(不重要,但很有趣)。 iOS 框架附带了四个即用型并发队列。您可以使用上面的示例或使用 Apple 的全局队列之一(通常推荐)创建并发队列:
let concurrentQueue = DispatchQueue.global(qos: .default)
RETAIN-CYCLE RESISTANT:调度队列是引用计数对象,但您不需要保留和释放全局队列,因为它们 是全局的,因此保留和释放被忽略。您可以访问 直接全局队列,无需将它们分配给属性。
调度队列有两种方式:同步和异步。
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
有点出乎意料,为什么并发队列要等待串行队列完成才执行?那不是并发。您的 Playground 可能会显示不同的输出,但我的 Playground 显示了这一点。它显示了这一点,因为我的并发队列的优先级不足以让 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 个可用队列。这可能看起来很多,但它们可以很快被使用,尤其是第三方库,尤其是数据库框架。出于这个原因,Apple 有关于队列管理的建议(在下面的链接中提到);一个是:
不是创建私有并发队列,而是将任务提交到其中之一 全局并发调度队列。 对于串行任务,设置 您的串行队列的目标到全局并发队列之一。 这样,您可以在保持队列的序列化行为的同时 最小化创建线程的单独队列的数量。
为此,Apple 建议不要像以前那样创建它们(您仍然可以),而是像这样创建串行队列:
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
【讨论】:
可能是 *** 上写的最好的答案❤️【参考方案4】:我喜欢用这个比喻来思考这个问题(这是原始图像的link):
假设你爸爸正在洗碗,而你刚喝了一杯苏打水。你把杯子拿给你爸爸清理,放在另一道菜旁边。
现在你爸爸一个人洗碗,所以他得一个一个洗:你爸爸在这里代表一个串行队列。
但你对站在那里看着它被清理并不真正感兴趣。所以,你放下杯子,回到你的房间:这称为异步调度。你的父亲可能会也可能不会在他完成后通知你,但重要的是你不是在等待玻璃被清理干净;你回到你的房间去做,你知道的,孩子们的事情。
现在让我们假设您仍然口渴,并且想在恰好是您最喜欢的同一个玻璃杯上放些水,并且您真的希望在它清理干净后立即将它拿回来。所以,你站在那里看着你爸爸洗碗,直到你洗完为止。这是一个同步调度,因为您在等待任务完成时被阻止了。
最后让我们说你妈妈决定帮助你爸爸并和他一起洗碗。现在队列变成了一个并发队列,因为他们可以同时清洗多个盘子;但请注意,无论它们如何工作,您仍然可以决定在那里等待或返回您的房间。
希望对你有帮助
【讨论】:
好吧,关于谁在做这件事,爸爸更像是一个线程?。不如给爸爸一台洗碗机来分派工作,这样他就可以真正排队了。【参考方案5】:如果我正确理解 GCD 的工作原理,我认为DispatchQueue
、serial
和concurrent
有两种类型,同时DispatchQueue
调度其任务的方式有两种,分配的closure
,第一个是async
,另一个是sync
。这些共同决定了闭包(任务)的实际执行方式。
我发现serial
和concurrent
表示队列可以使用多少个线程,serial
表示一个,而concurrent
表示多个。而sync
和async
表示任务将在哪个线程上执行,调用者的线程或该队列下的线程,sync
表示在调用者的线程上运行,而async
表示在底层线程上运行。
以下是可以在 Xcode Playground 上运行的实验代码。
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://***.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()
希望对您有所帮助。
【讨论】:
【参考方案6】:1.我正在阅读串行队列的创建和使用,以便一个接一个地执行任务。但是,如果发生以下情况会发生什么:- • 我创建了一个串行队列 • 我使用dispatch_async(在我刚刚创建的串行队列上)3 次调度三个块A、B、C
回答:- 所有三个块都一个接一个地执行。我创建了一个示例代码,有助于理解。
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)
【讨论】:
以上是关于GCD 中的并发队列与串行队列的主要内容,如果未能解决你的问题,请参考以下文章
GCD使用 串行并行队列 与 同步异步执行的各种组合 及要点分析
同步,异步,串行队列,并发队列,全局队列,主队列等概念的总结