GCD中的并发vs串行队列

时间:2013-10-04 10:47:22

标签: ios multithreading concurrency grand-central-dispatch

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

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

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

    这三个块是否会被执行:

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

      或者

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

    感谢您提供任何好的解释!

6 个答案:

答案 0 :(得分:176)

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

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

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

答案 1 :(得分:97)

我做了几个实验,让我了解这些serialconcurrent队列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")
        }
    }
}
  

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

  

以下是此实验的摘要

记住使用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")
        }
    }
}

编辑编辑:您可以观看演示视频here

答案 2 :(得分:13)

首先,了解线程和队列之间的区别以及GCD的实际作用很重要。当我们使用调度队列(通过GCD)时,实际上是在排队,而不是线程。正如Apple承认的那样,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
}

有两种类型的队列,即串行队列和并发队列,但所有队列都是相对并发的。您想“在后台”运行任何代码的事实意味着您想与另一个线程(通常是主线程)同时运行。因此,所有调度队列(串行或并发)均相对于其他队列并发执行任务。由队列(由串行队列)执行的任何序列化仅与单个[serial]调度队列中的任务有关(例如,在上面的示例中,同一串行队列中有两个任务;这些任务将在执行完一个之后另一个,永远不要同时出现。)

串行队列(通常称为专用调度队列)可确保任务从开始到结束一次执行一次,并按添加到该特定队列的顺序执行。 这是在调度队列讨论中任何地方都可以串行化的唯一保证 –特定串行队列中的特定任务是串行执行的。但是,如果串行队列是单独的队列,则它们可以与其他串行队列同时运行,因为同样,所有队列都相对于彼此并发。所有任务都在不同的线程上运行,但并非所有任务都可以保证在同一线程上运行(不重要,但很有趣)。而且iOS框架不附带任何现成的串行队列,您必须创建它们。私有(非全局)队列默认情况下是串行队列,因此要创建串行队列:

let serialQueue = DispatchQueue(label: "serial")

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

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

但是,在这一点上,如果您不向私有队列添加任何其他属性,Apple建议您仅使用它们的随时可用的全局队列之一(它们都是并发的)。

并发队列(通常称为全局调度队列)可以同时执行任务;但是,可以确保按照添加到特定队列的顺序启动,但是与串行队列不同,该队列在开始第二个任务之前不会等待第一个任务完成。任务(与串行队列一样)在不同的线程上运行,并且(并非与串行队列一样)并非每个任务都可以保证在同一线程上运行(不重要,但很有趣)。 iOS框架带有四个现成的并发队列。您可以使用以上示例创建并发队列,也可以仅使用Apple的全局队列之一(通常建议使用):

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

保持循环性:调度队列是引用计数的对象,但是您不必保留和释放全局队列,因为它们是全局的,因此保留和释放将被忽略。您可以直接访问全局队列,而不必将它们分配给属性。

有两种分配队列的方法:同步和异步。

同步调度是指调度队列后调度队列的线程(调用线程)在调度队列后暂停,并等待该队列块中的任务完成执行再恢复。要同步调度:

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

异步调度是指调用线程在调度队列后继续运行,并且不等待该队列块中的任务完成执行。要异步发送:

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

这有点意外,为什么并发队列在执行之前要等待串行队列完成?那不是并发的。您的游乐场可能显示不同的输出,但是我的显示了。它表明了这一点,因为我的并发队列的优先级还不够高,因此GCD不能更快地执行其任务。因此,如果我将所有内容保持不变,但是更改了全局队列的QoS(它的服务质量,这只是队列的优先级)let concurrentQueue = DispatchQueue.global(qos: .userInteractive),那么输出将是预期的:

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

两个串行队列按预期顺序串行执行任务,而并发队列则由于其优先级高(高QoS或服务质量)而更快地执行了任务。

两个并发队列(如我们的第一个打印示例中所示)显示混乱的打印输出(如预期的那样)。为了使它们能够连续清晰地打印,我们必须使它们两个都依次成为队列,并为其赋予相同的标签,以使它们成为相同的队列。然后,每个任务相对于另一个依次执行。但是,让它们串行打印的另一种方法是保持它们并发但更改其分派方法:

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

请记住,同步调度仅意味着调用线程在继续执行之前一直等待,直到队列中的任务完成为止。显然,这里的警告是,调用线程(在本例中为主线程)被冻结,直到第一个任务完成为止,这可能是也可能不是您希望UI执行的方式。对于寻求序列化的程序员来说,这不是一个很常见的解决方案,但是它确实有其用处-有时您确实想冻结UI一小段时间(通常不易察觉)(例如,当您希望防止用户在按钮中混入按钮时)。重要事件和简短事件的中间)。我可以列举更多示例,但您现在应该明白了。

最后,我认为重要的是要知道这些东西,就像编程中的所有东西一样,是相对原始的。根本没有什么复杂的(对吗?)-任何人都可以理解的东西。但是因为您还不知道,所以它有多简单也没关系,您根本就不知道。我之所以这样说,是因为不要被编程概念所吓倒,这很重要,因为当您需要依赖它们时,它将为您提供更好的处理方法。

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

答案 3 :(得分:4)

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

我发现syncserial表示队列可以使用的线程数,concurrent表示一个,而serial表示很多。并且concurrentsync意味着将在哪个线程,调用者的线程或该队列下面的线程async意味着在调用者的线程上运行任务而sync表示在底层线程上运行。

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

async

希望它有所帮助。

答案 4 :(得分:1)

我喜欢用这个比喻来思考这个问题(这里是原始图片的link):

Dad's gonna need some help

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

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

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

现在让我们假设您仍然口渴,想在您最喜欢的同一杯水上放些水,并且您真的希望它一洗干净就回来。因此,您站在那儿,看着爸爸洗碗,直到洗完为止。这是同步发送,因为您在等待任务完成时被阻止。

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

希望这会有所帮助

答案 5 :(得分:0)

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