同步/异步行为是否类似于串行/并发,即它们都控制调度队列,还是同步/异步只控制线程?

9
大多数stackoverflow上的答案都暗示同步和异步行为与串行和并发队列概念差异非常相似。就像@Roope的第一条评论中的链接所示。
我开始认为串行和并发与DispatchQueue有关,而同步/异步是关于操作在线程上执行方式的。 我是对的吗?
例如,如果我们使用“DQ.main.sync”,则任务/操作闭包将在此串行(主)队列上以同步方式执行。 而如果我使用“DQ.main.async”,则任务将在某个其他后台队列上异步执行,并在完成时返回控制权到主线程。 由于主线程是一个串行队列,因此它不会让任何其他任务/操作进入执行状态/开始执行,直到当前闭包任务完成执行为止。
然后,“DQ.global().sync”将在分配了其任务/操作的线程上同步执行任务,即通过阻止该特定线程上的任何上下文切换来阻止该线程执行任何其他任务/操作。 由于全局是一个并发队列,因此它将继续将其中存在的任务放置到执行状态,而不考虑先前的任务/操作的执行状态。
“DQ.global().async”将允许在将操作闭包放置到执行状态的线程上进行上下文切换。
以上的DispatchQueues和sync vs async的解释是否正确?

1
这个回答解决了你的问题吗?DispatchQueue.main.async和DispatchQueue.main.sync之间的区别 - Roope
1
@Roope 上面的链接也没有完整的区别,而且似乎相当含糊。它说:同步函数在任务完成后仅将控制权返回到当前队列。它会阻塞队列并等待任务完成。异步函数在任务被发送到不同队列执行后立即将控制权返回到当前队列。它不会等待任务完成,也不会阻塞队列。实际上,在上述文本中,同步和异步与串行和并发队列之间的差异非常相似。 - Aron
我不太明白你的意思,我链接的问题中的答案并不含糊。但是让我们来看看,你说“如果我在主队列上调用DQ.main.async,那么任务将在其他后台队列上异步执行,在完成时会将控制权返回到主线程”。你为什么认为在主队列上调用async会使该代码片段在除主队列之外的其他队列中执行?无论您在哪个队列上调用async/sync,都会执行代码。唯一的区别就是您当前的队列是否等待结果。 - Roope
1
@Aron,有没有一个答案对你最初的问题提供了适当的解释?如果是这样,请标记正确的解决方案为已接受,以通知感兴趣的人已经找到了解决方案。谢谢! - XLE_22
2个回答

10

您问的问题是正确的,但我认为您有点困惑(主要是因为关于这个主题的帖子在互联网上不是很清晰)。

并发/串行

让我们看看如何创建新的调度队列:

let serialQueue = DispatchQueue(label: label)

如果您不指定其他附加参数,此队列将表现为串行队列: 这意味着在此队列上分派的每个块(同步或异步无关紧要)都将单独执行,没有其他块可以同时在该队列上执行。 这并不意味着其他任何事情都停止了,只是意味着如果在同一队列上调度了其他内容,则它将等待第一个块完成后再开始执行。其他线程和队列仍会按照自己的方式运行。
但是,您可以创建一个并发队列,不会限制此代码块的方式,而是,如果在同一时间在该队列上分派了更多的代码块,它将同时执行它们(在不同的线程上)。
let concurrentQueue = DispatchQueue(label: label,
                      qos: .background,
                      attributes: .concurrent,
                      autoreleaseFrequency: .inherit,
                      target: .global())

因此,您只需要将属性concurrent传递给队列,它就不再是串行的了。

(我不会讨论其他参数,因为它们不是这个特定问题的重点,而且,我认为您可以在评论中链接的其他SO帖子中阅读有关它们的内容,或者如果这还不够,请提出另一个问题)


如果您想了解更多关于并发队列的信息(如果您不关心并发队列,请跳过此部分)

您可能会问:我什么时候需要使用并发队列呢?

好吧,比方说,让我们考虑一种使用情况,您想要同步共享资源的读操作:由于读操作可以同时进行而不会出现问题,您可以使用并发队列来实现。

但是,如果您想要在该共享资源上进行写入操作呢? 在这种情况下,写入需要充当“屏障”,在执行该写入期间,没有其他写入和读取可以同时对该资源进行操作。 为了获得这种行为,Swift代码可能如下所示:

concurrentQueue.async(flags: .barrier, execute: { /*your barriered block*/ })

换句话说,如果需要,您可以暂时将并发队列作为串行队列使用。
再次强调,所谓的并发/串行区别仅适用于分派到同一队列的块,与在其他线程/队列上执行的其他并发或串行工作无关。

同步/异步

这是完全不同的问题,与前面讨论的没有任何联系。
这两种方法用于调度某些代码块,相对于您在调用调度时所在的当前线程/队列。此调度调用会阻塞(同步)或不会阻塞(异步)该线程/队列的执行,同时在其他队列上执行您调度的代码。
比方说,我正在执行一个方法,在该方法中,我异步地将某些内容调度到其他队列上(我正在使用主队列,但它可以是任何队列):
func someMethod() {
    var aString = "1"
    DispatchQueue.main.async {
        aString = "2"
    }
    print(aString)
}

这段代码会被派发到另一个队列上,可以在该队列上串行或并发执行,但这与当前队列上正在发生的事情无关(当前队列是调用someMethod的队列)。
当前队列上的代码将继续执行,并且不会等待该代码块完成后再打印该变量。这意味着很可能会看到它打印1而不是2。(更准确地说,你不知道哪个先执行)
如果改为同步调度,则始终会打印 2,因为当前队列会等待该代码块完成后才继续执行。
所以这将打印2:
func someMethod() {
    var aString = "1"
    DispatchQueue.main.sync {
        aString = "2"
    }
    print(aString)
}

那么,这是否意味着在调用someMethod的队列实际上已经停止了?

嗯,这取决于当前的队列:

  • 如果是串行的,则是的。已经调度到该队列或将在该队列上调度的所有块都必须等待该块完成。
  • 如果是并发的,则不会。所有并发块将继续执行,只有特定的执行块将被阻塞,等待此调度调用完成其工作。当然,如果我们处于屏障情况下,则与串行队列一样。

当currentQueue和我们调度的队列相同时会发生什么?

假设我们正在使用串行队列(我认为这将是大多数用例)

  • 如果我们调度同步,则会死锁。没有任何东西会在那个队列上执行。这是最糟糕的情况。
  • 如果我们调度异步,则代码将在已经调度到该队列的所有代码之后执行(包括但不限于正在someMethod中执行的代码)

因此,在使用同步方法时要特别小心,确保您不在调度到同一队列中。

希望这可以让您更好地理解。


5
我开始认为串行和并发与DispatchQueue有关,同步/异步则决定了一个操作将在哪个线程上执行。简而言之:目标队列是串行还是并发决定了该目标队列的行为(即该队列是否可以同时运行已分派到该队列的其他任务),而sync/async则决定了从中分派的当前线程的行为(即是否应等待调用线程完成分派的代码)。因此,串行/并发影响您要分派到的目标队列,而sync/async影响您要从中分派的当前线程。你接着说:如果我们有DQ.main.sync,那么任务/操作闭包将在这个串行(主)队列上以同步方式执行。

我可能会重新表述为“如果我们有DQ.main.sync,那么当前线程将等待主队列执行此闭包。”


就我个人而言,我们很少使用DQ.main.sync,因为在大多数情况下,我们只是用它来分派一些UI更新,通常没有必要等待。这虽然微不足道,但我们几乎总是使用DQ.main.async。我们使用sync是为了提供与某些资源的线程安全交互。在那种情况下,sync非常有用。但通常不需要与main结合使用,只会引入低效。

此外,如果我执行DQ.main.async,则任务将在其他后台队列上异步运行,并在完成时将控制返回到主线程。

不是这样的。

当您执行DQ.main.async时,您正在指定闭包将异步运行在主队列(您分派的队列)上,并且您当前的线程(可能是后台线程)不需要等待它,而是会立即继续进行。

例如,考虑一个样例网络请求,其响应在URLSession的后台串行队列上处理:

let task = URLSession.shared.dataTask(with: url) { data, _, error in
    // parse the response
    DispatchQueue.main.async { 
        // update the UI
    }
    // do something else
}
task.resume()

因此,解析发生在这个URLSession后台线程上,它将UI更新调度到主线程中,然后继续在此后台线程上执行其他操作。 syncasync的整个目的是“做其他事情”是否必须等待“更新UI”完成。 在这种情况下,在主处理UI更新时阻塞当前后台线程没有意义,因此我们使用async

然后,DQ.global().sync会在分配了其任务/操作的线程上同步执行任务,即...

是的,DQ.global().sync表示“在后台队列上运行此闭包,但阻塞当前线程直到该闭包完成”。
毋庸置疑,在实践中,我们永远不会执行。阻塞当前线程等待全局队列的运行是没有意义的。将闭包分派到全局队列的整个目的就是为了不阻塞当前线程。如果您正在考虑使用,那么您可能想要在当前线程上运行它,因为无论如何都会阻塞它。(事实上,GCD知道没有任何作用,并且作为优化,通常会在当前线程上运行它。)
现在,如果您要使用或出于某种原因使用自定义队列,那么这可能是有意义的。但通常没有必要执行。
“...它将通过阻止在特定线程上进行任何其他任务/操作来阻塞该线程的上下文切换。” 这是不正确的。 sync 不影响“那个线程”(全局队列的工作线程)。sync 影响当前调度此代码块的线程。这个当前线程会等待全局队列执行已调度的代码(sync)还是不等待(async)?

而且,由于 global 是一个并发队列,它将继续将其中存在的任务放入执行状态,无论先前的任务/操作的执行状态如何。

是的。我可能会重新表述一下:“由于 global 是一个当前队列,此闭包将被安排立即运行,无论该队列上可能已经运行什么。”
技术上的区别在于,当您将某些内容分派到并发队列时,虽然它通常会立即启动,但有时候不会。也许您的 CPU 上的所有内核都被绑定运行其他东西。或者您已经调度了许多块,并且已经暂时耗尽了 GCD 非常有限的“工作线程”。总之,虽然它通常会立即启动,但始终可能存在资源约束阻止其这样做。
但这只是一个细节:概念上,当您调度到全局队列时,即使您可能有一些其他闭包已经被调度到该队列但尚未完成,它通常会立即开始运行。
“DQ.global().async”将允许在为执行操作闭包放置的线程上进行上下文切换。
我可能会避免使用“上下文切换”这个短语,因为它具有非常特定的含义,可能超出了这个问题的范围。如果你真的感兴趣,你可以看WWDC 2017视频现代化的Grand Central Dispatch使用
我描述“DQ.global().async”的方式是,它简单地“允许当前线程在全局队列执行分派的闭包时继续进行,而不被阻塞”。这是一种非常常见的技术,通常从主队列调用以将一些计算密集型代码分派到某些全局队列,但不等待其完成,使主线程空闲以处理UI事件,从而实现更响应的用户界面。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接