同步/异步的行为是否类似于串行/并发,即它们都控制 DispatchQueues 还是只控制线程

Aro*_*ron 9 grand-central-dispatch ios swift ios-multithreading dispatch-queue

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

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

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

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

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

这是对上述调度队列和同步与异步的正确解释吗?

Enr*_*oza 10

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

并发/串行

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

let serialQueue = DispatchQueue(label: label)
Run Code Online (Sandbox Code Playgroud)

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

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


但是,您可以创建一个并发队列,它不会以这种方式限制这些代码块,相反,如果同时在同一队列上调度更多代码块,它将在以下位置执行它们:同时(在不同线程上)

let concurrentQueue = DispatchQueue(label: label,
                      qos: .background,
                      attributes: .concurrent,
                      autoreleaseFrequency: .inherit,
                      target: .global())
Run Code Online (Sandbox Code Playgroud)

所以,你只需要将属性传递concurrent给队列,它就不再是串行的了。

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


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

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

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

但是如果您想在该共享资源上写入内容怎么办?好吧,在这种情况下,写入需要充当“屏障”,并且在执行该写入期间,没有其他写入和读取可以同时对该资源进行操作。为了获得这种行为,快速代码将如下所示

concurrentQueue.async(flags: .barrier, execute: { /*your barriered block*/ })
Run Code Online (Sandbox Code Playgroud)

因此,换句话说,您可以在需要时使并发队列暂时作为串行队列工作。


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

同步/异步

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

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

假设我正在执行一个方法,在该方法中我在其他队列上调度异步内容(我正在使用主队列,但它可以是任何队列):

func someMethod() {
    var aString = "1"
    DispatchQueue.main.async {
        aString = "2"
    }
    print(aString)
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

所以这将打印 2:

func someMethod() {
    var aString = "1"
    DispatchQueue.main.sync {
        aString = "2"
    }
    print(aString)
}
Run Code Online (Sandbox Code Playgroud)

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

嗯,这取决于当前队列:

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

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

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

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

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

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


Rob*_*Rob 5

\n

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

\n
\n

简而言之:

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

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

    \n
  • \n
\n

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

\n

你接着说:

\n
\n

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

\n
\n

我可能会将其改写为 \xe2\x80\x9c 如果我们已经得到,DQ.main.sync那么当前线程将等待主队列执行此关闭。\xe2\x80\x9d

\n
\n

FWIW,我们不DQ.main.sync经常使用\xe2\x80\x99,因为十有八九,我们\xe2\x80\x99只是这样做来调度一些UI更新,并且\xe2\x80\x99通常不需要等待。它\xe2\x80\x99 是次要的,但我们几乎总是使用DQ.main.async. sync当我们\xe2\x80\x99尝试提供与某些资源的线程安全交互时,我们确实使用is。在这种情况下,sync可能非常有用。但它通常不需要与 结合使用main,而只会导致效率低下。

\n
\n

而且,如果我DQ.main.async这样做,任务将在其他后台队列上异步获取,并且在完成后将返回主线程的控制权。

\n
\n

不。

\n

当您这样做时DQ.main.async,您\xe2\x80\x99指定闭包将在主队列(您分派到的队列)上异步运行,并且您当前的线程(大概是后台线程)不需要\xe2\x80\x99等待它,但会立即继续。

\n

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

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

因此,解析发生在这个URLSession后台线程上,它将 UI 更新分派到主线程,然后在此后台线程上继续执行其他操作。syncvs的全部目的async是 \xe2\x80\x9c 执行其他操作 \xe2\x80\x9d 是否必须等待 \xe2\x80\x9c 更新 UI\xe2\x80\x9d 完成。在这种情况下,当主线程正在处理 UI 更新时,\xe2\x80\x99s 没有必要阻止当前后台线程,因此我们使用async.

\n
\n

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

\n
\n

是的DQ.global().sync,\xe2\x80\x9crun 这个闭包在后台队列上运行,但会阻塞当前线程,直到该闭包完成。\xe2\x80\x9d

\n

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

\n

现在,如果您出于某种原因打算使用async或正在使用某些自定义队列,那么这可能是有意义的。但是,\xe2\x80\x99s 通常没有意义DQ.global().sync

\n
\n

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

\n
\n

不。

\n

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

\n
\n

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

\n
\n

是的。再次,我可能会改写: \xe2\x80\x9c 并且,由于global是当前队列,因此无论此队列上可能已经运行什么,都会安排此闭包立即运行。\xe2\x80\x9d

\n

技术区别在于,当您将某些内容分派到并发队列时,虽然它通常会立即启动,但有时它不会\xe2\x80\x99t。也许 CPU 上的所有核心都在运行其他东西。或者,也许您\xe2\x80\x99已经分派了许多块,并且您\xe2\x80\x99暂时耗尽了GCD\xe2\x80\x99s非常有限的\xe2\x80\x9c工作线程\xe2\x80\x9d。最重要的是,虽然它通常会立即启动,但总是可能存在资源限制阻止它这样做。

\n

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

\n
\n

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

\n
\n

我可能会避免使用短语\xe2\x80\x9c上下文切换\xe2\x80\x9d,因为它具有非常具体的含义,可能超出了这个问题的范围。如果您\xe2\x80\x99确实感兴趣,可以观看 WWDC 2017 视频现代化 Grand Central Dispatch 使用

\n

我\xe2\x80\x99d描述的方式DQ.global().async是,它只是\xe2\x80\x9c允许当前线程继续,不受阻塞,而全局队列执行分派闭包。\xe2\x80\x9d这是一种非常常见的技术,通常从主队列调用以将一些计算密集型代码分派到某个全局队列,但不等待其完成,从而使主线程可以自由地处理 UI 事件,从而获得响应更快的用户界面。

\n