GCD中的并发与串行队列


117

我正在努力完全理解GCD中的并发和串行队列。我遇到了一些问题,希望有人能及时明确地回答我。

  1. 我正在阅读串行队列的创建和使用,以便依次执行任务。但是,如果发生以下情况,会发生什么情况:

    • 我创建一个串行队列
    • 我使用dispatch_async了3次(在我刚刚创建的串行队列中)分配了三个块A,B,C

    将执行以下三个块:

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

      要么

    • 并发(同时在并行线程上),因为我使用了ASYNC调度
  2. 我读到我可以dispatch_sync在并发队列上使用,以便一个接一个地执行块。在那种情况下,为什么甚至不存在串行队列,因为我始终可以使用并发队列,在该队列中可以同步分配任意数量的块?

    感谢您的任何解释!


一个简单的先决条件好问题,即调度同步还是异步
蜂蜜

Answers:


216

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

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

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


14
因此,您告诉我:(1)队列的类型(conc或serial)是决定任务是按顺序执行还是并行执行的ONLY元素;(2)调度类型(同步还是异步)只是在说执行是执行还是不执行下一条指令?我的意思是,如果我分派任务SYNC,则无论执行在什么队列上,代码都将阻塞直到任务完成。
Bogdan Alexandru

13
@BogdanAlexandru正确。队列决定执行策略,而不是如何对块进行排队。同步等待块完成,异步不等待。
2013年

2
@swiftBUTCHER是的。创建队列时,可以指定最大线程数。如果添加的任务少于这些任务,那么它们将并行执行。除此之外,某些任务将一直排在队列中,直到有可用的容量为止。
斯蒂芬·达林顿

2
@PabloA。,主线程是一个串行队列,因此实际上只有两种情况。除此之外,它是完全相同的。异步立即返回(该块可能在当前运行循环的末尾执行)。主要问题是,如果您确实要从主线程同步主线程,在这种情况下会出现死锁。
斯蒂芬·达林顿

1
@ShauketSheikh否。主线程是一个串行队列,但并非所有串行队列都是主线程。第四点,主线程将阻塞,等待另一个线程参与其工作。如果串行队列是主线程,则会出现死锁。
斯蒂芬·达林顿,

122

这里有几个,我已经做了,使我明白这些实验中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中使用异步时,任务将在其他线程(主线程除外)中运行。异步意味着执行下一行不要等到块执行后才执行,这将导致非阻塞主线程和主队列。由于它的串行队列,所有的任务都按照添加到串行队列中的顺序执行。串行执行的任务总是一次由与该队列关联的单个线程一次执行。

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在给定的队列上运行一个块并等待其完成,这会阻塞主线程或主队列。由于主队列需要等待直到分派的块完成,所以主线程将可用于处理非队列中的块。因此,有可能在后台队列上执行的代码实际上可能在主线程上执行。

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中使用异步时,任务将在后台线程中运行。异步意味着执行下一行不要等到块执行完毕才执行非阻塞主线程。请记住,在并发队列中,任务是按照添加到队列中的顺序进行处理的,但是队列中附加了不同的线程。请记住,它们不应该按照添加到队列中的顺序来完成任务。每次自动创建线程时,任务的顺序都会有所不同。任务是并行执行的。达到(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只是将任务添加到队列中并从该队列中执行任务。队列根据操作是同步还是异步在主线程或后台线程中分派任务。队列类型为串行,并发,主调度队列。默认情况下,您执行的所有任务都是从主调度队列完成的。已经有四个预定义的全局并发队列供您的应用程序使用,以及一个主队列(DispatchQueue.main)。也可以手动创建自己的队列并从该队列执行任务。

与UI相关的任务应始终通过将任务分派到Main队列来从主线程执行。快捷方式实用程序是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")
        }
    }
}

编辑:您可以在此处观看演示视频


很棒的演示.... 下一行不要等到块执行后才导致非阻塞主线程,这就是为什么如果您在后台线程上使用断点,则会跳至的}原因,因为那时它实际上并未在执行
Honey

@那个懒惰的iOS家伙웃我仍然不了解异步并发与异步串行之间的区别。使用其中任何一个意味着什么。它们都在后台运行,不会打扰用户界面。为何还要使用同步?并非所有代码都同步。一个接一个地?
eonist

1
@GitSyncApp,您可以在此处
Anish Parajuli웃

// @那个懒惰的iOS Guy웃:谢谢。我在松弛的swift-lang上发布。将是could如果您也可以创建DispatchGroup和DispatchWorkItem。:D
杰出主义者

我已经测试你的最后一个,concurrentQueue.syncdoLongSyncTaskInConcurrentQueue()功能,它打印主线程,Task will run in different thread似乎并非如此。
gabbler's

54

首先,了解线程和队列之间的区别以及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建议您仅使用它们的随时可用的全局队列之一(它们都是并发的)。在此答案的底部,您将看到另一种创建串行队列的方法(使用target属性),这是Apple建议的方式(为了更有效地进行资源管理)。但是目前,贴上标签就足够了。

并发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

这有点出乎意料,为什么并发队列在执行之前要等待串行队列完成?那不是并发。您的游乐场可能显示不同的输出,但是我的显示了。它表明了这一点,因为我的并发队列的优先级还不够高,因此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执行的方式。

因此,我们无法执行以下操作:

DispatchQueue.main.sync { ... }

这是我们无法执行的队列和调度方法的唯一可能的组合-主队列上的同步调度。那是因为我们要冻结主队列,直到我们在大括号内执行任务为止……然后将其分派到刚刚冻结的主队列中。这称为死锁。要在操场上观看它的动作:

DispatchQueue.main.sync { // stop the main queue and wait for the following to finish
    print("hello world") // this will never execute on the main queue because we just stopped it
}
// deadlock

最后要提到的是资源。当我们给队列一个任务时,GCD从其内部管理的池中找到一个可用的队列。就此答案的编写而言,每个qos有64个队列可用。这似乎很多,但是它们很快就会被消耗,尤其是第三方库,尤其是数据库框架。因此,Apple提出了有关队列管理的建议(在下面的链接中提到);一个是:

代替创建私有并发队列,将任务提交到全局并发调度队列之一。对于串行任务,请将串行队列的目标设置为全局并发队列之一。 这样,您可以在最小化创建线程的单独队列数量的同时,维护队列的序列化行为。

为此,Apple建议像这样创建串行队列,而不是像以前那样创建它们(您仍然可以)。

let serialQueue = DispatchQueue(label: "serialQueue", qos: .default, attributes: [], autoreleaseFrequency: .inherit, target: .global(qos: .default))

为了进一步阅读,我建议以下内容:

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

https://developer.apple.com/documentation/dispatch/dispatchqueue


7

如果我理解正确有关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()

希望对您有所帮助。


7

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

爸爸需要帮忙

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

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

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

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

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

希望这可以帮助


3

1.我正在阅读串行队列的创建和使用,以便一个接一个地执行任务。但是,如果发生以下情况,会发生什么情况:-•创建一个串行队列•我使用dispatch_async(在刚创建的串行队列上)三次,以分派三个块A,B,C

解答:-所有三个块一个接一个地执行。我创建了一个有助于理解的示例代码。

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)
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.