main.async vs main.sync() vs global().async 在 Swift3 GCD

19

示例A:这会导致应用程序崩溃。

DispatchQueue.main.async {           
    let url = URL(string: imageUrl)
    do {
         let data = try Data(contentsOf: url!)
            DispatchQueue.main.sync {
                self.imageIcon.image = UIImage(data: data)
            }
        }

例子B:但这并不意味着
DispatchQueue.global().async {  
    let url = URL(string: imageUrl)
    do {
        let data = try Data(contentsOf: url!)
            DispatchQueue.main.sync {
                self.imageIcon.image = UIImage(data: data)
            }
        }

根据我的了解:
- `x.sync` 表示在主线程/界面线程中执行任务。 - `x.async` 表示在后台线程中执行任务。 - `Global` 表示使用并发队列执行某个任务,即并行任务。
问题1:为什么当我在后台线程上执行任务(例如 `main.async`),然后调用主线程来更新界面时,我的应用程序会崩溃?
问题2:`main.async` 和 `global().async` 之间有什么区别吗?

4
你似乎混淆了术语,main指的是主线程,global则是后台线程,sync表示按顺序执行任务(在队列中如主线程或后台线程),async是并发执行任务(同样是在单个队列上并行执行任务)。我曾在StackOverflow文档上找到过一篇很好的解释,但现在他们已经关闭了 :( - Fonix
2
"x.sync 意味着在主线程/UI 线程执行任务,而 x.async 意味着在后台线程执行。我认为这不是正确的,你也不应该把队列当作线程(它们并不相同)。调用 async 意味着它不会阻塞,但你正在调用 DispatchQueue.main,这是一个保证在主线程上运行的队列。" - Ssswift
谢谢您的帮助,我已经发布了我的最终结论,请您检查一下。 - Shivam Srivastava
4个回答

25

简单来说,我得出结论:

  • 队列 - 有三种类型的队列,即1个主队列、4个全局队列和任意数量的自定义队列。
  • 线程 - 一条是主线程,另外的后台线程由系统提供给我们。

DispatchQueue.main.async

-它意味着在主队列中使用后台线程执行任务(不阻塞UI),当任务完成时,它会自动更新到UI,因为它已经在主队列中了。

DispatchQueue.global().async 以及 global().sync

这意味着在全局队列中使用后台线程执行任务,并且当任务完成时,global().sync用于将工作从globalQueue带到mainQueue,从而更新UI。

我的应用程序崩溃的原因

我试图通过(main.sync)将已完成的任务带到MainQueue,但它已经在MainQueue上,因为我没有切换队列,这创建死锁(MainQueue等待它自己),导致我的应用程序崩溃。


抱歉,但是这个回答有很多错误。为什么会有这么多赞? - HangarRash

21

GCD

Thread -> GCD -> Operation + OperationQueue(life cycle, dependencies between different queues, cancel)

[同步 vs 异步]
[iOS 线程安全]

Grand Central Dispatch(GCD) libdispatch 在调度队列DispatchQueue上操作。它按照先进先出(FIFO)顺序工作。

DispatchQueue.<queue>.<sync/async> 表示在<queue>上运行一个<sync/async>任务

队列可以是串行的或并发的。并发(更加并行)允许同时处理多个任务,而串行则一个接一个地处理。并发任务按照添加的顺序启动,但可以以不同的顺序完成。

GCD 支持:

  • 主队列 - 应用程序主线程上的串行队列,用于处理UI
  • 全局队列 - 在整个iOS操作系统之间共享的并发队列
  • 私有队列 - 应用程序范围内的串行/并发队列

主队列

//Thread.current.qualityOfService = .userInitiated
DispatchQueue.main

全球队列

DispatchQueue.global(qos: DispatchQoS.QoSClass = .default)

私人队列
DispatchQueue(label: String, qos: DispatchQoS = .unspecified, attributes: DispatchQueue.Attributes = [], autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit, target: DispatchQueue? = nil)

//Serial Queue
DispatchQueue(label: "first.myApp.com") 

//Concurrent Queue - attributes: .concurrent
DispatchQueue(label: "second.myApp.com", attributes: .concurrent)       

服务质量(QoS)

每个队列都有一个qos,它决定了其优先级。

  • userInteractive - 最高优先级 - 速度优先于能耗。任务的结果会反映在用户界面上。任务应该小而快速产生结果。例如动画。
  • userInitiated - 用户启动的任务,其结果是异步的。例如加载数据/打开本地文件以在用户界面上显示结果。
  • utility - 长时间运行的计算,最多几分钟,例如保存数据到文件...
  • background - 最低优先级 - 能耗优先于速度。非常长时间运行的计算,几分钟以上,例如同步数据。

应避免使用的其他QoS:

  • default - 优先级介于.userInitiated和.utility之间。
  • unspecified - 使用外部线程的QoS(继承调用者的QoS)。例如当您从主线程调用时,thread.qualityOfService == .userInitiated
func examineQoS(qos: DispatchQoS) {
    let queue = <create_queue>
    queue.async {
        print("""
                QoS
                qos: \(qos.qosClass)
                queue.qos: \(queue.qos.qosClass)
                thread.qualityOfService: \(Thread.current.qualityOfService)
                """)
    }
}

let queue = DispatchQueue.global()
let queue = DispatchQueue(label: "mySerial")
let queue = DispatchQueue(label: "myConcurrent", attributes: .concurrent)
//qos == .default
//queue.qos: unspecified
//thread.qualityOfService: .userInitiated

let queue = DispatchQueue.global(qos: qos.qosClass)
    
//qos: default
//queue.qos: unspecified
//thread.qualityOfService: .userInitiated

//qos: unspecified
//queue.qos: unspecified
//thread.qualityOfService: .userInitiated

//Others
//qos == queue.qos == thread.qualityOfService

let queue = DispatchQueue(label: "mySerial", qos: qos)
let queue = DispatchQueue(label: "myConcurrent", qos: qos, attributes: .concurrent)

//qos: default
//queue.qos: default
//thread.qualityOfService: .default

//qos: unspecified
//queue.qos: unspecified
//thread.qualityOfService: .userInitiated

//Others
//qos == queue.qos == thread.qualityOfService
  • 工作线程的数量取决于操作系统的条件。工作线程没有运行循环。

同步/异步

同步 - 阻塞当前线程,并等待在指定队列上完成

异步 - 不阻塞当前线程,并将代码执行块发送到指定队列

//deadline in seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
    // logic
}

常见错误:死锁

如果您在main线程上调用DispatchQueue.main.sync - 应用程序将被冻结,因为调用DispatchQueue.main.sync时,调度的块完成后立即开始等待(调度的块尚未启动)

一些注意事项:

  • DispatchWorkItem - 在QueueDispatchGroup中延迟/取消/优先执行任务
  • DispatchGroup,如果您要在不同的队列上执行多个异步任务并使用单个回调。所有这些任务应该被分组。 DispatchGroup包含线程安全计数器,当它等于0时,调用notify
//create group
let group = DispatchGroup()

//case 1
DispatchQueue.<queue>.async(group: group) //

//case 2 - manual
group.enter() //<- +1
DispatchQueue.global().async { 
    //logic
    group.leave() //<- -1
}

//notification
group.notify(queue: <callback_queue>) {
    //logic             
}
  • Barrier标志位在并发队列中用于同步/异步任务,确保没有竞态条件[关于]。最好的位置是自定义队列,因为它不会阻塞其他全局任务:
customQueue.async(flags: .barrier) { 
    //logic
    someProperty = someValue
}

所有已开始的任务都已完成 单一屏障任务 执行队列中的所有其他任务
通过并发队列中的屏障可以实现线程安全操作,用于共享变量:
读取 - 并发队列上的同步操作 写入 - 使用屏障的异步操作 [线程安全的单例模式]

8
在第一种情况下,您在main上运行代码,然后在主线程上使用main.sync。实质上,您试图告诉main队列等待自己-这显然是荒谬的,因此会导致崩溃。
在第二种情况下,您在后台线程上运行代码,然后使用main.sync等待main线程能够运行main.sync中提供的块。
总的来说,我会始终使用async而不是sync,除非必须使用sync。并且始终将一个线程(DispatchQueue)与另一个线程同步,永远不要与同一个线程同步。

2
是的,sync 意味着等待直到该块执行完成,而 async 则不会等待。在主线程上使用 sync 时必须小心,否则它会阻塞 UI 的更新。 - Fonix
我知道 global().async 的意思是在后台执行任务,但是 main.async 是什么意思呢..?? - Shivam Srivastava

4
您混淆了“同步/异步”和“主线程/全局线程”的术语。
同步 - 同步运行某些任务(即可以是主线程/全局线程/任何其他线程,将等待任务完成)
异步 - 异步运行某些任务(即可以是主线程/全局线程/任何其他线程,将把任务推入队列并继续执行块外的下一步。它不会等待)
现在让我们来逐个解释导致您代码崩溃的原因:
我们给线程起一些名称以便更好的理解:
1)ThreadA - 遇到您的调度语句的线程(这也可以是主线程,但为了说明目的,我认为最好是这样)
2)ThreadB - 提交某些任务时创建的全局线程。
3)ThreadMain - 主线程
演示例子:
DispatchQueue.main.async - ThreadA来执行此语句并将您的块放在ThreadMain上,并继续移动到下一个步骤(因为它是异步的)。

现在让我们来谈谈ThreadMain,从这里它将做什么。由于ThreadMain得到一个块(由ThreadA提交),它开始逐步执行,突然看到'DispatchQueue.main.sync'并将内部块提交到相同的ThreadMain队列上并一直等待(因为它是同步的)。所以你实际上让ThreadMain进入了死锁状态。

主.async在后台执行任务,然后与MainThread同步工作,我说得对吗? - Shivam Srivastava
@ShivamSrivastava 不是的。main.async 不会在后台执行任务,而是在主队列/线程上执行。 - HangarRash

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