首页 文章

GCD中的并发vs串行队列

提问于
浏览
90

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

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

  • 我创建了一个串行队列

  • 我使用 dispatch_async (在我刚刚创建的串行队列上)三次调度三个块A,B,C

这三个块会被执行:

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

要么

  • 同时(在parralel线程上同时)因为我使用了ASYNC调度

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

谢谢你的任何好解释!

5 回答

  • 97

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

    • async - concurrent:代码在后台线程上运行 . 控件立即返回主线程(和UI) . 该块可以是该队列上运行的唯一块

    • async - serial:代码在后台线程上运行 . 控制立即返回主线程 . 该块可以假定它是该队列上运行的唯一块

    • sync - concurrent:代码在后台线程上运行,但主线程等待它完成,阻止对UI的任何更新 . 该块可以是该队列上运行的唯一块(我可以在几秒前使用异步添加另一个块)

    • sync - serial:代码在后台线程上运行,但主线程等待它完成,阻止对UI的任何更新 . 该块可以假设它是该队列上运行的唯一块

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

  • 168

    以下是一些实验,让我了解这些 serialconcurrentGrand 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")
            }
        }
    }
    

    在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")
            }
        }
    }
    

    在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")
        }
    }
    

    在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")
        }
    }
    

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

    Here is a summary of this experiments

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

    UI相关任务应始终从主线程执行,方法是将任务调度到主队列 . 快速手工实用程序是 DispatchQueue.main.sync/async ,而网络相关/重型操作应始终异步完成,无论是哪个线程都使用主要或后台

    EDIT: However, There are cases you need to perform network calls operations synchronously in a background thread without freezing UI(e.g.refreshing OAuth Token and wait if it succeed or not).You need to wrap that method inside a asynchronous operation.This way your heavy operations are executed in the order and without Blocking main thread.

    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")
            }
        }
    }
    

    EDIT EDIT: 你可以观看演示视频here

  • 4

    首先,了解线程和队列之间的区别以及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
    }
    

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

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

    let serialQueue = DispatchQueue(label: "serial")
    

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

    let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])
    

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

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

    let concurrentQueue = DispatchQueue.global(qos: .default)
    

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

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

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

    DispatchQueue.global(qos: .default).sync {
        // task goes in here
    }
    

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

    DispatchQueue.global(qos: .default).async {
        // task goes in here
    }
    

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

    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)
        }
    
    }
    

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

    let serialQueue = DispatchQueue(label: "serial")
    let concurrentQueue = DispatchQueue.global(qos: .default)
    

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

    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
    

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

    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
    

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

    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
    

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

    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
    

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

    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
    

    那个_2617976不是并发的 . 你的游乐场可能会显示不同的输出但我的显示了这个 . 它显示了这一点,因为我的并发队列足够高,GCD可以提前触发队列 . 因此,如果我保持一切相同,但更改全局队列的QoS let concurrentQueue = DispatchQueue.global(qos: .userInteractive) ,则输出符合预期:

    1
    1001
    1002
    1003
    2
    1004
    1005
    3
    4
    5
    101
    102
    103
    104
    105
    

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

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

    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
    

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

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

  • 0

    如果我正确理解GCD是如何工作的,我认为有两种类型的 DispatchQueueserialconcurrent ,同时,有两种方式 DispatchQueue 如何调度其任务,分配 closure ,第一种是 async ,另一种是 sync . 这些一起决定了闭包(任务)实际执行的方式 .

    我发现 serialconcurrent 表示队列可以使用的线程数, serial 表示一个,而 concurrent 表示很多 . 并且 syncasync 表示任务将在哪个线程,调用者的线程或该队列下的线程上执行, sync 表示在调用者的线程上运行,而 async 表示在底层线程上运行 .

    以下是可以在Xcode playground上运行的实验代码 .

    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()
    

    希望它能有所帮助 .

  • 8
    **1.    I'm reading that serial queues are created and used in order to execute tasks one after the other. However, what happens if:
        •   I create a serial queue
        •   I use dispatch_async (on the serial queue I just created) three times to dispatch three blocks A,B,C**
    **ANSWER**:-
    All three blocks executed one after the another. I have created one sample code that helps to understand
    
    let serialQueue = DispatchQueue(label: "SampleSerialQueue")
    //Block first
    serialQueue.async {
        for i in 1...10{
            print("Serial - First operation",i)
        }
    }
    //Block second
    serialQueue.async {
        for i in 1...10{
            print("Serial - Second operation",i)
        }
    }
    //Block Third
    serialQueue.async {
        for i in 1...10{
            print("Serial - Third operation",i)
        }
    }
    

相关问题