4

stackoverflow 上的大多数答案都暗示同步与异步行为与串行与并发队列概念差异非常相似。就像@Roope 的第一条评论中的链接一样

我已经开始认为 Serial 和 concurrent 与DispatchQueue和 sync/async 有关如何在线程上执行操作。我对吗?

就像如果我们有DQ.main.sync那么任务/操作闭包将在这个串行(主)队列上以同步方式执行。而且,如果我这样做了,DQ.main.async那么任务将在其他一些后台队列上异步获取,并且在完成时将返回对主线程的控制。而且,由于 main 是一个串行队列,它不会让任何其他任务/操作进入执行状态/开始执行,直到当前的闭包任务完成执行。

然后, DQ.global().sync将在已分配其任务/操作的线程上同步执行任务,即,它将通过阻止该特定线程上的任何上下文切换来阻止该线程执行任何其他任务/操作。而且,由于 global 是一个并发队列,它将继续将其中存在的任务置于执行状态,而不管先前的任务/操作的执行状态如何。

DQ.global().async将允许在已放置操作闭包以执行的线程上进行上下文切换

这是对上述 dispatchQueues 和同步与异步的正确解释吗?

4

2 回答 2

5

您提出了正确的问题,但我认为您有些困惑(主要是由于互联网上有关此主题的帖子不太清楚)。

并发/串行

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

let serialQueue = DispatchQueue(label: label)

如果您不指定任何其他附加参数,则此队列将表现为串行队列:这意味着在此队列上调度的每个块(同步或异步无关紧要)将单独执行,而没有其他块的可能性在同一个队列上同时执行。

这并不意味着其他任何东西都停止了,它只是意味着如果在同一个队列上调度了其他东西,它将等待第一个块完成后再开始执行。其他线程和队列仍将自行运行。


但是,您可以创建一个并发队列,它不会以这种方式约束这些代码块,相反,如果同时在同一个队列上调度更多代码块,它将在同时(在不同的线程上)

let concurrentQueue = DispatchQueue(label: label,
                      qos: .background,
                      attributes: .concurrent,
                      autoreleaseFrequency: .inherit,
                      target: .global())

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

(我不会谈论其他参数,因为它们不是这个特定问题的焦点,我认为,您可以在评论中链接的其他 SO 帖子中阅读它们,或者,如果还不够,您可以问另一个问题)


如果您想了解有关并发队列的更多信息(又名:如果您不关心并发队列,请跳过)

你可能会问:我什么时候需要并发队列?

好吧,举个例子,让我们考虑一个您想要在共享资源上同步 READS 的用例:由于读取可以同时完成而不会出现问题,因此您可以为此使用并发队列。

但是如果你想在那个共享资源上写呢?好吧,在这种情况下,写入需要充当“屏障”,并且在执行该写入期间,没有其他写入和读取可以同时对该资源进行操作。为了获得这种行为,快速代码看起来像这样

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

因此,换句话说,您可以将并发队列临时作为串行队列工作,以备不时之需。


再一次,并发/串行的区别仅对分派到同一队列的块有效,它与可以在另一个线程/队列上完成的其他并发或串行工作无关。

同步/异步

这完全是另一个问题,与前一个问题几乎没有任何联系。

这两种分派代码块的方法与您在分派调用时所处的当前线程/队列有关。在执行您在另一个队列上分派的代码时,此调度调用会阻塞(在同步的情况下)或不阻塞(异步)该线程/队列的执行。

因此,假设我正在执行一个方法,并且在该方法中我在其他队列上调度异步某些东西(我正在使用主队列,但它可以是任何队列):

func someMethod() {
    var aString = "1"
    DispatchQueue.main.async {
        aString = "2"
    }
    print(aString)
}

发生的情况是,这段代码被分派到另一个队列上,并且可以在该队列上串行或并发执行,但这与当前队列(调用 someMethod 的队列)上发生的事情无关。

当前队列上发生的情况是代码将继续执行,并且在打印该变量之前不会等待该块完成。这意味着,您很可能会看到它打印 1 而不是 2。(更准确地说,您不知道首先会发生什么)

相反,如果您将其同步调度,那么您将始终打印 2 而不是 1,因为当前队列会等待该代码块完成,然后再继续执行。

所以这将打印 2:

func someMethod() {
    var aString = "1"
    DispatchQueue.main.sync {
        aString = "2"
    }
    print(aString)
}

但这是否意味着调用 someMethod 的队列实际上已停止?

好吧,这取决于当前队列:

  • 如果是连续的,那就是。先前分派到该队列或将在该队列上分派的所有块都必须等待该块完成。
  • 如果它是并发的,那就不是。所有并发块将继续执行,只有这个特定的执行块会被阻塞,等待这个调度调用完成它的工作。当然,如果我们处于屏障的情况下,那么它就像串行队列一样。

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

假设我们在串行队列上(我认为这将是您的大部分用例)

  • 如果我们调度同步,而不是死锁。该队列将不再执行任何操作。这是可能发生的最糟糕的情况。
  • 如果我们调度异步,那么代码将在该队列上已经调度的所有代码的末尾执行(包括但不限于现在在 someMethod 中执行的代码)

因此,当您使用同步方法时要格外小心,并确保您不在您正在分派的同一个队列中。

我希望这能让你更好地理解。

于 2019-11-18T13:38:56.967 回答
3

我开始认为串行和并发与 DispatchQueue 相关,而同步/异步与如何在线程上执行操作有关。

是的,串行或并发队列的选择决定了您要分派到的队列的行为,但sync/async与该代码在另一个队列上的运行方式无关。相反,它决定了您从中分派的线程的行为。所以,简而言之:

  • 目标队列是串行的还是并发的,决定了目标队列的行为方式(即,该队列是否可以与其他被分派到同一队列的事物同时运行此闭包);

  • syncvsasync规定了您从中分派的当前线程的行为方式(即,调用线程是否应该等到分派的代码完成)。

因此,串行/并发会影响您要分派到的目标队列sync/会影响您从中async分派的当前线程。

你接着说:

就像如果我们有DQ.main.sync那么任务/操作闭包将在这个串行(主)队列上以同步方式执行。

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

请记住,“同步方式”与目标队列(您的示例中的主队列)中发生的事情无关DQ.main.sync,而是与您调用的线程有关sync。当前线程是否要等待?

FWIW,我们不DQ.main.sync经常使用,因为 10 次中有 9 次,我们这样做只是为了调度一些 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 更新分派到主线程,然后在这个后台线程上继续做其他事情。syncvs的全部目的async是“做其他事情”是否必须等待“更新 UI”完成。在这种情况下,没有必要在 main 处理 UI 更新时阻塞当前的后台线程,所以我们使用async.

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

YesDQ.global().sync表示“在后台队列上运行这个闭包,但是在闭包完成之前阻塞当前线程。”

不用说,在实践中,我们永远不会这样做DQ.global().sync。阻塞当前线程等待在全局队列上运行是没有意义的。将闭包分派到全局队列的重点是不要阻塞当前线程。如果您正在考虑DQ.global().sync,您不妨只在当前线程上运行它,因为无论如何您都在阻止它。(事实上​​,GCD 知道这DQ.global().sync并不能实现任何目标,并且作为一种优化,通常无论如何都会在当前线程上运行它。)

现在,如果您出于某种原因要使用async或使用一些自定义队列,那么这可能是有道理的。但一般来说,这样做是没有意义的DQ.global().sync

...它将通过阻止该特定线程上的任何上下文切换来阻止该线程执行任何其他任务/操作。

不。

sync不会影响“那个线程”(全局队列的工作线程)。会sync影响您从中分派此代码块的当前线程。当前线程会等待全局队列执行分派的代码(sync)还是不(async)?

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

是的。再一次,我可能会改写:“而且,因为global是当前队列,所以这个闭包将被安排立即运行,不管这个队列上可能已经运行了什么。”

技术上的区别在于,当您将某些内容分派到并发队列时,虽然它通常会立即启动,但有时不会。也许你 CPU 上的所有内核都在运行其他东西。或者也许你已经调度了很多块并且你暂时耗尽了 GCD 非常有限数量的“工作线程”。最重要的是,虽然它通常会立即开始,但总会有资源限制阻止它这样做。

但这是一个细节:从概念上讲,当您分派到全局队列时,是的,它通常会立即开始运行,即使您可能已经分派到该队列的其他一些闭包尚未完成。

DQ.global().async将允许在已放置操作闭包以执行的线程上进行上下文切换。

我可能会避免使用“上下文切换”这个短语,因为它具有非常具体的含义,可能超出了这个问题的范围。如果您真的感兴趣,可以观看 WWDC 2017 视频Modernizing Grand Central Dispatch Usage

我要描述的方式DQ.global().async是它只是“允许当前线程继续进行,不受阻塞,而全局队列执行分派的闭包。” 这是一种非常常见的技术,通常从主队列调用以将一些计算密集型代码分派到某个全局队列,但不等待它完成,让主线程可以自由处理 UI 事件,从而产生更灵敏的用户界面。

于 2019-12-30T18:14:34.040 回答