GCD中的并发vs串行队列

Bog*_*dru 106 concurrency multithreading grand-central-dispatch ios

我正在努力完全理解GCD中的并发和串行队列.我有一些问题,希望有人能够清楚地回答我.

  1. 我正在读取串行队列的创建和使用,以便一个接一个地执行任务.但是,如果出现以下情况:

    • 我创建了一个串行队列
    • 我使用dispatch_async(在我刚创建的串行队列上)三次来发送三个块A,B,C

    这三个块会被执行:

    • 按顺序A,B,C,因为队列是串行的

      要么

    • 同时(在parralel线程上同时)因为我使用了ASYNC调度
  2. 我正在阅读我可以dispatch_sync在并发队列上使用,以便一个接一个地执行块.在这种情况下,为什么串行队列甚至存在,因为我总是可以使用并发队列,我可以根据需要同步调度多个块?

    谢谢你的任何好解释!

Ste*_*ton 191

一个简单的例子:你有一个需要一分钟才能执行的块.您将它从主线程添加到队列中.让我们来看看这四个案例.

  • async - concurrent:代码在后台线程上运行.控件立即返回主线程(和UI).该块不能假定它是该队列上运行的唯一块
  • async - serial:代码在后台线程上运行.控制立即返回主线程.该块可以假设它是该队列上运行的唯一块
  • sync - concurrent:代码在后台线程上运行,但主线程等待它完成,阻止对UI的任何更新.该块不能假设它是该队列上运行的唯一块(我可以在几秒前使用异步添加另一个块)
  • sync - serial:代码在后台线程上运行,但主线程等待它完成,阻止对UI的任何更新.该块可以假设它是该队列上运行的唯一块

显然,对于长时间运行的进程,你不会使用后两者中的任何一个.当您尝试从可能在另一个线程上运行的某些内容更新UI(始终在主线程上)时,通常会看到它.

  • 所以你告诉我:(1)队列的类型(conc或serial)是决定任务是按顺序执行还是按顺序执行的唯一元素; (2)调度类型(同步或异步)仅表示执行是否进入或未进入下一条指令?我的意思是,如果我发送任务SYNC,代码将阻塞,直到任务完成,无论它执行什么队列? (10认同)
  • @BogdanAlexandru正确.队列指示执行策略,而不是如何对块进行排队.同步等待块完成,异步不等. (10认同)
  • 感谢Stephen和@Jano,现在一切都很有意义! (2认同)
  • @swiftBUTCHER 在某种程度上,是的。创建队列时,您可以指定最大线程数。如果您添加的任务少于它们将并行执行的任务。除此之外,一些任务将保留在队列中,直到有可用容量。 (2认同)
  • @PabloA.,主线程是一个串行队列,所以实际上只有两种情况.除此之外,它完全一样.异步立即返回(并且块可能在当前运行循环结束时执行).主要问题是如果你确实将_from_主线程同步到主线程,在这种情况下你会遇到死锁. (2认同)

LC *_*C 웃 111

以下是我做过的一些实验,让我了解这些serial,concurrent排队Grand Central Dispatch.

 func doLongAsyncTaskInSerialQueue() {

   let serialQueue = DispatchQueue(label: "com.queue.Serial")
      for i in 1...5 {
        serialQueue.async {

            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

当您在GCD中使用async时,任务将在不同的线程(主线程除外)中运行.异步意味着执行下一行不要等到块执行,这导致非阻塞主线程和主队列.由于它的串行队列,所有都按照它们被添加到串行队列的顺序执行.串行执行的任务总是由与队列相关联的单个线程一次执行一个.

func doLongSyncTaskInSerialQueue() {
    let serialQueue = DispatchQueue(label: "com.queue.Serial")
    for i in 1...5 {
        serialQueue.sync {
            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

当您在GCD中使用同步时,任务可以在主线程中运行.Sync在给定队列上运行一个块并等待它完成,这导致阻塞主线程或主队列.由于主队列需要等到调度块完成,主线程将可用于处理来自除以外的队列的块.因此,在后台队列上执行的代码可能实际上正在主线程上执行.因为它的串行队列,所有都按它们被添加的顺序执行(FIFO).

func doLongASyncTaskInConcurrentQueue() {
    let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    for i in 1...5 {
        concurrentQueue.async {
            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
        print("\(i) executing")
    }
}
Run Code Online (Sandbox Code Playgroud)

当您在GCD中使用async时,任务将在后台线程中运行.异步意味着执行下一行不要等到块执行导致非阻塞主线程.请记住,在并发队列中,任务按照它们添加到队列的顺序进行处理,但是队列中附加了不同的线程.记住它们不应该按照它们添加到队列中的顺序来完成任务.每次创建线程时,任务的顺序都会不同.任务是并行执行的.当达到(maxConcurrentOperationCount)以上时,某些任务将作为一个串行,直到一个线程空闲.

func doLongSyncTaskInConcurrentQueue() {
  let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    for i in 1...5 {
        concurrentQueue.sync {
            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
        print("\(i) executed")
    }
}
Run Code Online (Sandbox Code Playgroud)

当您在GCD中使用同步时,任务可以在主线程中运行.Sync在给定队列上运行一个块并等待它完成,这导致阻塞主线程或主队列.由于主队列需要等到调度块完成,主线程将可用于处理来自除以外的队列的块.主队列.因此,在后台队列上执行的代码可能实际上正在主线程上执行.由于其并发队列,任务可能无法按照它们添加到队列的顺序完成.但是使用同步操作它虽然可以由不同的线程处理.因此,它的行为就像这是串行队列一样.

以下是此实验的摘要

记住使用GCD时,您只是将任务添加到队列并从该队列执行任务.队列根据操作是同步还是异步,在主线程或后台线程中调度您的任务.队列类型是Serial,Concurrent,Main dispatch queue.您执行的所有任务默认都是从Main dispatch queue完成的.您的应用程序已经有四个预定义的全局并发队列和一个主队列(DispatchQueue.main).你是也可以手动创建自己的队列并从该队列执行任务.

UI相关任务应始终从主线程执行,方法是将任务分派到主队列.快速手工实用程序是DispatchQueue.main.sync/async网络相关/重型操作应该始终异步完成没有任何线程你使用主要或后台

编辑:但是,有些情况下你需要在后台线程中同步执行网络调用操作而不冻结UI(例如刷新OAuth令牌并等待它是否成功).你需要将该方法包装在异步操作中.这样你的重量级操作按顺序执行,无需阻塞主线程.

func doMultipleSyncTaskWithinAsynchronousOperation() {
    let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    concurrentQueue.async {
        let concurrentQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default)
        for i in 1...5 {
            concurrentQueue.sync {
                let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
                let _ = try! Data(contentsOf: imgURL)
                print("\(i) completed downloading")
            }
            print("\(i) executed")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑编辑:你可以在这里观看演示视频


bso*_*sod 29

首先,了解线程和队列之间的区别以及GCD实际上的作用非常重要.当我们使用调度队列(通过GCD)时,我们实际上是排队,而不是线程.Dispatch框架专门设计用于让我们远离线程,因为Apple承认"实现正确的线程解决方案[可能]变得非常困难,如果不是[有时]不可能实现的话." 因此,要同时执行任务(我们不想冻结UI的任务),我们需要做的就是创建这些任务的队列并将其交给GCD.GCD处理所有相关的线程.因此,我们所做的只是排队.

立即知道的第二件事是任务是什么.任务是该队列块中的所有代码(不在队列中,因为我们可以一直向队列添加内容,但是在我们将其添加到队列的闭包中).任务有时被称为块,块有时被称为任务(但它们通常被称为任务,特别是在Swift社区中).无论代码多少,代码中的所有代码都被视为单个任务:

serialQueue.async {
    // this is one task
    // it can be any number of lines with any number of methods
}
serialQueue.async {
    // this is another task added to the same queue
    // this queue now has two tasks
}
Run Code Online (Sandbox Code Playgroud)

有两种类型的队列,串行和并发,但所有队列都是并发的,相对于彼此.您希望"在后台"运行任何代码这一事实意味着您希望与另一个线程(通常是主线程)同时运行它.因此,所有调度队列(串行或并发)相对于其他队列同时执行其任务.队列(通过串行队列)执行的任何序列化只与该单个[串行]调度队列中的任务有关(如上例中同一串行队列中有两个任务;这些任务将在一个后执行另一个,从不同时).

串行队列(通常称为专用调度队列)保证按照将它们添加到特定队列的顺序一次执行一个任务.序列化的唯一保证是特定队列中的特定任务是串行完成的; 如果两个串行队列是单独的队列,则它们可以同时运行.任务在不同的线程上运行,但不是每个任务都保证在同一个线程上运行.iOS框架没有任何现成的串行队列,您必须制作它们.私有(非全局)队列默认是串行的,因此要创建一个串行队列:

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

您可以通过其属性属性使其并发:

let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])
Run Code Online (Sandbox Code Playgroud)

但此时,如果您没有向私有队列添加任何其他属性,Apple建议您只使用其中一个即用型全局队列(全部并发).

并发队列(通常称为全局调度队列)可以同时执行任务(但是,任务保证按照将它们添加到该特定队列的顺序启动).任务(与串行队列一样)在不同的线程上运行(与串行队列一样)并非每个任务都保证在同一个线程上运行.iOS框架附带了四个即用型并发队列.您可以使用上面的示例创建并发队列,或者只使用Apple的全局队列之一:

let concurrentQueue = DispatchQueue.global(qos: .default)
Run Code Online (Sandbox Code Playgroud)

调度队列是引用计数的对象,但您不需要保留和释放全局队列,因为它们是全局的,因此忽略了保留和释放.您可以直接访问全局队列,而无需将它们分配给属性.

调度队列有两种方式:同步和异步.

同步调度意味着调度队列的线程(调用线程)在调度队列后暂停,并等待该队列块中的任务在恢复之前完成执行.要同步发送:

DispatchQueue.global(qos: .default).sync {
    // task goes in here
}
Run Code Online (Sandbox Code Playgroud)

异步调度意味着调用线程在调度队列后继续运行,并且不等待该队列块中的任务完成执行.要异步调度:

DispatchQueue.global(qos: .default).async {
    // task goes in here
}
Run Code Online (Sandbox Code Playgroud)

现在有人可能会认为,为了以串行方式执行任务,应该使用串行队列,这并不完全正确.为了以串行方式执行多个任务,应该使用串行队列,但是所有任务(由它们自己隔离)都是串行执行的.考虑这个例子:

whichQueueShouldIUse.syncOrAsync {

    for i in 1...10 {
        print(i)
    }
    for i in 1...10 {
        print(i + 100)
    }
    for i in 1...10 {
        print(i + 1000)
    }

}
Run Code Online (Sandbox Code Playgroud)

无论您如何配置(串行或并发)或调度(同步或异步)此队列,此任务将始终以串行方式执行.第三个循环永远不会在第二个循环之前运行,第二个循环永远不会在第一个循环之前运行.在任何使用任何调度的队列中都是如此.当你引入串行和并发发挥作用的多个任务/队列时.考虑这两个队列,一个串行和一个并发:

let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue.global(qos: .default)
Run Code Online (Sandbox Code Playgroud)

假设我们在异步中调度两个并发队列:

concurrentQueue.async {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
101
2
102
103
3
104
4
105
5
Run Code Online (Sandbox Code Playgroud)

他们的输出是混乱的,但请注意他们都是连续执行自己的任务.现在让我们制作第一个串口:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

101
1
2
102
3
103
4
104
5
105
Run Code Online (Sandbox Code Playgroud)

是不是第一个应该串行执行的队列?这是(第二次).在后台发生的其他事情与队列无关.我们告诉串行队列执行串行执行但它确实......但是我们只给了它一个任务.现在让我们给它两个任务:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
2
3
4
5
101
102
103
104
105
Run Code Online (Sandbox Code Playgroud)

但是如果我们将它们分成两个独立的串行队列(因为在上面的示例中它们是相同的队列),它们的输出再次混乱:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue2.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
101
2
102
3
103
4
104
5
105
Run Code Online (Sandbox Code Playgroud)

但是就你而言,没有任何改变,因为每个串行队列仍在串行执行,但每个队列只有一个任务,所以没有什么可以"串行".现在让我们回到两个串行队列(同一队列)并添加第三个队列,一个并发队列:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 1000)
    }
}

1
2
3
4
5
101
102
103
104
105
1001
1002
1003
1004
1005
Run Code Online (Sandbox Code Playgroud)

这有点出乎意料,为什么并发队列在执行之前等待串行队列完成?那不是并发性.你的游乐场可能会显示不同的输出但我的显示了这个.它显示了这一点,因为我的并发队列的优先级不够高,GCD无法提前触发队列.因此,如果我保持一切相同,但更改全局队列的QoS let concurrentQueue = DispatchQueue.global(qos: .userInteractive),则输出符合预期:

1
1001
1002
1003
2
1004
1005
3
4
5
101
102
103
104
105
Run Code Online (Sandbox Code Playgroud)

两个串行队列以串行方式执行其任务(如预期的那样),并发队列由于其较高的优先级(与其他队列交错)而更快地完成了它的任务.

两个并发队列,如我们的第一个打印示例,显示混乱的打印输出(如预期的那样).为了让它们以串行方式整齐地打印,我们必须将它们更改为串行队列并将它们放在同一队列中.然后每个都相对于另一个执行.让它们以串行方式打印的另一种方法是保持它们并发但改变它们的分派方法:

concurrentQueue.sync {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
2
3
4
5
101
102
103
104
105
Run Code Online (Sandbox Code Playgroud)

这种方法的警告是调用线程(在这种情况下是主线程)被冻结,直到第一个任务完成,因为队列是同步调度的.对于寻求序列化的程序员来说,这不是一个常见的解决方案,但它确实有其用途.我可以通过更多的例子,但你现在应该得到这个想法.

https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091-CH1-SW1

  • 可能是 StackOverflow 上写过的最好答案❤️ (3认同)

小智 5

如果我理解正确有关GCD是如何工作的,我觉得有两种类型的DispatchQueueserial并且concurrent,在同一时间,有两个办法如何DispatchQueue派遣其任务,分配的closure,第一个是async,另一个是sync。这些共同决定了关闭(任务)的实际执行方式。

我发现,serialconcurrent平均多少线程队列可以使用,serial手段之一,而concurrent意味着许多。并且sync并且async意味着任务将在哪个线程,调用者的线程或该队列下sync的线程上执行,意味着在调用者的线程上async运行,而意味着在底层线程上运行。

以下是可以在Xcode操场上运行的实验代码。

PlaygroundPage.current.needsIndefiniteExecution = true
let cq = DispatchQueue(label: "concurrent.queue", attributes: .concurrent)
let cq2 = DispatchQueue(label: "concurent.queue2", attributes: .concurrent)
let sq = DispatchQueue(label: "serial.queue")

func codeFragment() {
  print("code Fragment begin")
  print("Task Thread:\(Thread.current.description)")
  let imgURL = URL(string: "http://stackoverflow.com/questions/24058336/how-do-i-run-asynchronous-callbacks-in-playground")!
  let _ = try! Data(contentsOf: imgURL)
  print("code Fragment completed")
}

func serialQueueSync() { sq.sync { codeFragment() } }
func serialQueueAsync() { sq.async { codeFragment() } }
func concurrentQueueSync() { cq2.sync { codeFragment() } }
func concurrentQueueAsync() { cq2.async { codeFragment() } }

func tasksExecution() {
  (1...5).forEach { (_) in
    /// Using an concurrent queue to simulate concurent task executions.
    cq.async {
      print("Caller Thread:\(Thread.current.description)")
      /// Serial Queue Async, tasks run serially, because only one thread that can be used by serial queue, the underlying thread of serial queue.
      //serialQueueAsync()
      /// Serial Queue Sync, tasks run serially, because only one thread that can be used by serial queue,one by one of the callers' threads.
      //serialQueueSync()
      /// Concurrent Queue Async, tasks run concurrently, because tasks can run on different underlying threads
      //concurrentQueueAsync()
      /// Concurrent Queue Sync, tasks run concurrently, because tasks can run on different callers' thread
      //concurrentQueueSync()
    }
  }
}
tasksExecution()
Run Code Online (Sandbox Code Playgroud)

希望对您有所帮助。


Yun*_*hel 5

我喜欢用这个隐喻来思考(这是原始图像的链接):

爸爸需要帮忙

假设您的父亲正在洗碗,而您刚喝了一杯苏打水。您将玻璃杯带到您的父亲那里进行清理,将其放在其他盘子旁。

现在,您的父亲自己一个人洗碗,所以他将必须一个一个地做饭:您的父亲在这里代表一个连续的队列

但是您并不真正有兴趣站在那里并看着它被清理干净。因此,您放下玻璃杯,然后回到您的房间:这称为异步分配。爸爸做完事后可能会通知您,也可能不会通知您,但重要的一点是您不必等待玻璃被清理干净。你回到自己的房间去做,孩子的事情。

现在,让我们假设您仍然口渴,想在您最喜欢的同一杯水上放些水,并且您真的希望在清洗后尽快将其倒回去。因此,您站在那儿,看着爸爸洗碗,直到洗完为止。这是一个同步调度,因为您在等待任务完成时被阻止。

最后,假设您的母亲决定帮助您的父亲,并与他一起洗碗。现在,该队列成为并发队列,因为它们可以同时清洗多个餐具。但请注意,无论它们如何工作,您仍然可以决定在那里等待或返回房间。

希望这可以帮助