为什么带同步的并发队列表现得像串行队列?

Why concurrent queue with sync act like serial queue?

谁能帮助我理解我创建的这段代码:

let cq = DispatchQueue(label: "downloadQueue", attributes: .concurrent)
cq.sync {
for i in 0..<10 {
    sleep(2)
    print(i)
  }
}

print("all finished!")

并且输出序列顺序 1->10,中间等待 2 秒。最后会打印出all finished

最后一部分我明白了。 但是我的问题是:

并发队列不应该同时启动多个任务吗? 所以我最初的想法是:1-10的打印应该做concurrently,不一定要按顺序打印。

谁能解释 sync 调用并发队列的目的并举例说明为什么以及何时需要它?

不行,你必须向 DispatchQueue 添加多个操作,然后它会 运行 并行处理多个队列。

如果您想同时演示它们 运行ning,您应该单独分派这 10 个任务:

let cq = DispatchQueue(label: "downloadQueue", attributes: .concurrent)

for i in 0..<10 {
    cq.async {
        sleep(2)
        print(i)
    }
}
print("all finished queuing them!")

注:

  • 并发队列有 10 个分派,没有一个。

    每个分派的任务 运行 与分派到该队列的其他任务并发(这就是为什么我们需要多次分派来说明并发性)。

  • 另请注意,我们是异步调度的,因为我们不想在调度下一个任务之前调用队列等待每个已调度的任务。


你问:

So my original thought was: the 1-10 printing should be done concurrently, not necessarily in the serial order.

因为它们在单个调度中,所以它们将 运行 作为单个任务,按顺序 运行ning。您需要将它们放在单独的调度中才能同时看到它们 运行。

你接着问:

Could anyone explain the purpose of sync call on concurrent queue and give me an example why and when we need it?

sync与目标队列是串行还是并发无关。 sync 仅指示 calling 线程的行为,即调用者是否应该等待分派的任务完成。在这种情况下,你真的不想等待,所以你应该使用 async.

作为一般规则,您应该避免调用 sync,除非 (a) 您绝对必须这样做;和 (b) 您愿意阻塞调用线程直到 sync 任务 运行s。所以,除了极少数例外,应该使用 async。而且,也许不用说,我们绝不会阻塞主线程超过几毫秒。

虽然通常避免在并发调度队列上使用 sync,但您可能会遇到的一个例子是“reader-writer”同步模式。在这种情况下,“读取”是同步发生的(因为您需要等待结果),但是“写入”是异步发生的并带有屏障(因为您不需要等待,但您不希望它与任何事情同时发生其他在那个队列上)。使用 GCD 进行同步的详细讨论(尤其是 reader-writer 模式)可能超出了这个问题的范围。但是在网络或 Whosebug 上搜索“GCD reader-writer”,您会发现有关该主题的讨论。)


让我们以图形方式说明我修改后的代码再现,使用 OSLog 在 Instruments 的“兴趣点”工具中创建间隔:

import os.log

private let log = OSLog(subsystem: "Foo", category: .pointsOfInterest)

class Foo {
    func demonstration() {
        let queue = DispatchQueue(label: "downloadQueue", attributes: .concurrent)

        for i in 0..<10 {
            queue.async { [self] in
                let id = OSSignpostID(log: log)
                os_signpost(.begin, log: log, name: "async", signpostID: id, "%d", i)
                spin(for: 2)
                os_signpost(.end, log: log, name: "async", signpostID: id)
            }
        }
        print("all finished queuing them!")
    }

    func spin(for seconds: TimeInterval) {
        let start = CACurrentMediaTime()
        while CACurrentMediaTime() - start < seconds { }
    }
}

当我在仪器(例如“产品”»“配置文件”)中对此进行分析时,选择“时间分析器”模板(其中包括“兴趣点”工具),我看到了正在发生的事情的图形时间轴:

所以让我提请您注意以上两个有趣的方面:​​

  1. 并发队列运行的任务并发,但是因为我的iPhone的CPU只有六个核心,所以实际上只有六个可以运行 同时。接下来的四个必须等到核心可用于该特定工作线程。

    请注意,此演示之所以有效,是因为我不只是调用 sleep,而是在旋转所需的时间间隔,以更准确地模拟一些缓慢的阻塞任务。自旋比 sleep 更适合慢速同步任务。

  2. 这说明,正如您所指出的,并发任务可能不会按照提交它们的精确顺序出现。这是因为(a)他们都接连排队的速度太快了;和 (b) 他们 运行 同时:存在一个“竞赛”,即哪个 运行ning 线程首先到达日志语句(或“兴趣点”间隔)。

    最重要的是,对于那些同时 运行 的任务,由于竞争,它们可能不会 运行 按顺序出现。这就是并发执行的工作原理。