阅读 5791

iOS - 多线程分析之 DispatchQueue Ⅰ

Dispatch ( 全称 Grand Central Dispatch,简称 GCD ) 是一套由 Apple 编写以提供让代码以多核并发的方式执行应用程序的框架。

DispatchQueue ( 调度队列 ) 就是被定义在 Dispatch 框架中,可以用来执行跟多线程有关操作的类。

在使用它之前,我们得先了解一下基本概念,我会先简单介绍,后面再根据讲解的内容逐步详细介绍,目的是为了方便读者融入。

PS:如果在阅读时发现有任意错误,请指点我,感谢!

同步和异步执行

同步和异步执行

如图。同步和异步的区别在于,线程会等待同步任务执行完成;线程不会等待异步任务执行完成,就会继续执行其他任务/操作。

阅读指南:

本文中出现的 "任务" 是指 sync {}async {} 中整个代码块的统称,"操作" 则是在 "任务" 中执行的每一条指令 ( 代码 ) ;因为主线程没有 "任务" 之说,主线程上执行的每一条 ( 段 ) 代码,都统称为 "操作"。

串行和并发队列

在 GCD 中,任务由**队列 (串行或并发) **负责管理和决定其执行顺序,在一条由系统自动分配的线程上执行。

串行 (Serial) 队列中执行任务时,任务会按照固定顺序执行,执行完一个任务后再继续执行下一个任务 (这意味着串行队列同时只能执行一个任务) ;在并发 (Concurrent) 队列中执行任务时,任务可以同时执行 ( 其实是在以极短的时间内不断的切换线程执行任务 ) 。

串行和并发队列都以 先进先出 (FIFO) 的顺序执行任务,任务的执行流程如图:

任务执行流程

示例1 - 在串行队列中执行同步 ( sync ) 任务

// 创建一个队列(默认就是串行队列,不需要额外指定参数)
let queue = DispatchQueue(label: "Serial.Queue")

print("thread: \(Thread.current)")

queue.sync {
    (0..<5).forEach { print("rool-1 -> \($0): \(Thread.current)") }
}

queue.sync {
    (0..<5).forEach { print("rool-1 -> \($0): \(Thread.current)") }
}

/**
 thread: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-1 -> 0: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-1 -> 1: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-1 -> 2: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-1 -> 3: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-1 -> 4: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-2 -> 0: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-2 -> 1: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-2 -> 2: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-2 -> 3: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-2 -> 4: <NSThread: 0x281951f40>{number = 1, name = main}
 */
复制代码

没什么好解释的,结果肯定是按照正常的顺序来,一个接着一个地执行。因为同步执行就是会一直等待,等到一个任务全部执行完成后,再继续执行下一个任务。

有一点需要注意的是,主线程和在同步任务中 Thread,current 的打印结果相同,也就是说,队列中的同步任务在执行时,系统给它们分配的线程是主线程,因为同步任务会让线程等待它执行完,既然会等待,那就没有再开辟线程的必要了。

关于主线程和主队列

当应用程序启动时,就有一条线程被系统创建,与此同时这条线程也会立刻运行,该线程通常叫做程序的主线程

同时系统也为我们提供一个名为主队列 ( DispatchQueue.main {} ) 的串行特殊队列,默认我们写的代码都处于主队列中,主队列中的所有任务都在主线程执行。

示例2 - 在串行队列中执行异步 ( async ) 任务

let queue = DispatchQueue(label: "serial.com")

print("thread: \(Thread.current)")

(0..<50).forEach {
    print("main - \($0)")
    // 让线程休眠0.2s,目的是为了模拟耗时操作,不再赘述。
    Thread.sleep(forTimeInterval: 0.2)
}

queue.async {
    (0..<5).forEach {
        print("rool-1 -> \($0): \(Thread.current)")
        Thread.sleep(forTimeInterval: 0.2)
    }
}

queue.async {
    (0..<5).forEach {
        print("rool-2 -> \($0): \(Thread.current)")
        Thread.sleep(forTimeInterval: 0.2)
    }
}

/**
 thread: <NSThread: 0x281251fc0>{number = 1, name = main}
 main - 0
 main - 1
 main - 2 ... 顺序执行到 49
 rool-1 -> 0: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-1 -> 1: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-1 -> 2: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-1 -> 3: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-1 -> 4: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-2 -> 0: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-2 -> 1: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-2 -> 2: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-2 -> 3: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-2 -> 4: <NSThread: 0x281234100>{number = 3, name = (null)}
 */
复制代码

可以看到,线程一定会等待它当前的操作 ( 包括让线程休眠 ) 执行完后,再继续执行 async 任务。此时任务同样按顺序执行,因为串行队列只能执行完一个任务后再继续执行下一个任务。

任务中 Thread.current 的打印结果都是 number = 3 ,换句话说,串行队列中的异步任务在执行时,系统给它们开辟的线程是其他线程,并且只开辟一个,因为串行队列同时只能执行一个任务,因此没有开启多条线程的必要。

关于让线程休眠

这里解释一下 Thread.sleep 这个方法的作用:是让当前线程暂停任何操作0.2s。

请注意我说的是当前线程,不要误以为是让整个应用程序都停止了,不是这样的。如果当前任务所在的线程停止了,是不会影响到别的线程正在执行任务的,这点要区分清楚。

PS:也就是说,在上面同步任务中,为了测试而调用的 Thread.sleep 方法并没有作用 ( 但是为了测试和验证,依然调用了 ) ,因为任务都在一条线程上,并按照固定顺序执行。

示例3 - 在串行队列中执行异步 ( async ) 任务 II

let queue = DispatchQueue(label: "serial.com")
print("1: \(Thread.current)")
queue.async { print("2: \(Thread.current)") }
print("3: \(Thread.current)")
queue.async { print("4: \(Thread.current)") }
print("5: \(Thread.current)")

/**
 1: <NSThread: 0x28347ed00>{number = 1, name = main}
 3: <NSThread: 0x28347ed00>{number = 1, name = main}
 2: <NSThread: 0x2834268c0>{number = 3, name = (null)}
 5: <NSThread: 0x28347ed00>{number = 1, name = main}
 4: <NSThread: 0x2834268c0>{number = 3, name = (null)}
 */
复制代码

这时候打印的顺序并不固定,但肯定会先从 1 开始打印,打印的结果可能是:12345, 12354, 13254, 13245, 13524, 13254... ,这是为什么?我们先来了解一些概念后再来回顾。

队列和任务的关系

首先要解释一下同步异步这两个词的概念,既然是同步或异步,也能解释为相同,或是不同,它需要一个作为参照的对象,来知道它们相对于这个对象来说到底是相同,还是不同。

那在 GCD 中,它们的参照对象就是我们的主线程 ( dispatchQueue.main ) 。也就是说如果是同步任务,那就在主线程执行;而如果是异步任务,那就在其他线程执行

这就解释了,为什么串行队列在执行异步任务时,还会开启线程,所谓异步嘛,那就是不在主线程执行,区别是串行队列只会开启一条线程,而并发队列会开启多条线程

而同步任务是,甭管它是什么队列和任务,只要执行的是同步任务,就在主线程执行

  • 异步任务

    异步任务说:“我要开始执行任务了,快给我分配线程让我执行。”

    应用程序说:“好!我另外开辟线程出来让你执行,等等,请问你所处的队列是?”

    异步任务说:“串行队列。”

    应用程序说:“既然是串行队列,而串行队列中的所有任务都会按照固定顺序执行,只能执行完一个任务后再继续执行下一个任务 ( 这意味着串行队列同时只能执行一个任务 ) ,那我就只给你分配一条线程吧!你队列中的所有任务、包括你,都在这条线程上顺序执行。”

    异步任务说:“那如果我处在并发队列中呢?”

    应用程序说:“如果是在并发队列中,那队列中的所有任务可以同时执行,我会给你分配多条线程,让每个任务可以在不同的线程上同时执行。”

  • 同步任务

    同步任务说:“我要开始执行任务了,快给我分配线程让我执行。”

    应用程序说:“既然是同步任务那就相当于在主线程执行,那我就给你主线程来执行吧!”

    同步任务说:“我的待遇太差了。”

任务和线程的关系

任务只有两种,同步任务和异步任务,无论同步任务是处在什么队列中,它都会让当前正在执行的线程等待它执行完成,例如:

// 当前线程执行打印 main-1 的操作
print("main-1")

// 线程执行到这里发现遇到一个 sync 任务,就会在此等待,
// 直到 sync 任务执行完成,才会继续执行其他操作。
//
// 串行或并发队列
queue.sync {
    (0..<10).forEach {
        print("sync \($0): \(Thread.current)")
        Thread.sleep(forTimeInterval: 0.5)
    }
}
// 等待!线程等待 sync 执行完后,再继续执行打印 main-2 的操作。
print("main-2")

/**
 main-1
 sync 0: <NSThread: 0x6000011968c0>{number = 1, name = main}
 sync 1: <NSThread: 0x6000011968c0>{number = 1, name = main}
 sync 2: <NSThread: 0x6000011968c0>{number = 1, name = main}
 sync 2 ...9
 main-2
*/
复制代码

而如果是异步任务,不管它处在什么队列中,当前线程都不会等待它执行完成,例如:

// 当前线程执行打印 main-1 的操作
print("main-1")

// 线程执行到这里发现遇到一个 async 任务,
// 那么线程不会等待它执行完成,就会继续执行其他操作。
//
// 串行或并发队列
queue.async {
    (0..<20).forEach { print("async \($0)") }
}

// 开辟线程的时间大约是90微妙,加上循环的准备以及打印时间,
// 这里给它200微妙,测试async任务中的线程和当前线程之间的执行顺序。
Thread.sleep(forTimeInterval: 0.0002000)

// 不会等待!线程不会等待 async 执行完成就会执行打印 main-2 的操作
print("main-2")
复制代码

打印的结果可能稍有不同,但是肯定先从 main-1 开始打印。虽然 main-2 是执行在 async 后面的,async 也会先执行,但是由于当前线程不等待它执行完成的机制,所以它在执行到某一刻时如果到了线程需要打印 main-2 的时间,就会执行打印 main-2 的操作。也有可能是,main-2 先执行,然后等到了某一时刻再执行 async 中的任务 ( 开辟线程需要时间 ) 。

也就是说,这里当前线程和 async 任务中的线程在执行时是不阻塞对方的 ( 互不等待 ) ,本次运行结果如下:

/**
main-1
async 0
async 1
async 2
main-2
async 3
async 4
async 5
...
*/
复制代码

PS:我是怎么知道开辟线程的时间大约是 90 微妙的?因为我看了线程成本中的描述。

回顾

这就能解释之前示例中的执行顺序了,再来回顾一下:

let queue = DispatchQueue(label: "serial.com")
print("1: \(Thread.current)")
queue.async { print("2-\(Thread.current)") }
print("3: \(Thread.current)")
queue.async { print("4: \(Thread.current)") }
print("5: \(Thread.current)")
复制代码

虽然执行顺序不固定,但还是有一定的规律可循的,因为是串行队列,所以在主线程中 1, 3, 5 一定按顺序执行,而在 async 线程中 2, 4 也一定按顺序执行。

示例4 - 串行队列死锁

首先,并发队列不会出现死锁的情况;其次,在串行队列中,只有 sync { sync {} }async { sync {} } 会出现死锁,内部的 sync closure 永远不会被执行,并且程序会崩溃,例如:

queue.sync {
    print("1")
    queue.sync { print("2") }
    print("3")
}
// Prints "1"

queue.async {
    print("1")
    queue.sync { print("2") }
    print("3")
}
// Prints "1"
复制代码

仔细观察上面的代码就会发现,只有内部套用 sync {} 的情况下才会死锁,那使用 sync ( 同步 ) 意味着什么呢?这意味着,当前线程会等待同步任务执行完成。可问题是,这个 sync 任务是嵌套在另一个任务里面的 ( sync { sync {} } ) ,那这里就有两个任务了。

由于串行队列是执行完当前任务后,再继续执行下一个任务。放到这里就是,内部的 sync {} 想要执行的话,它必须要等待外部的 sync {} 执行完成,那外部的 sync {} 能不能执行完成呢?由于这个内部任务是同步的,它会阻塞当前正在执行外部 sync {} 的线程,让当前线程等待它 ( 内部 sync {} ) 执行完成,可问题是外部的 sync {} 完成不了的话,内部的 sync {} 也无法执行,结果就是一直等待,谁都无法继续执行,造成死锁。

既然线程会等待内部的同步任务执行完成,又限制串行队列同时只能执行一个任务,那在外部的 sync {} 没有执行完成之前,内部的 sync {} 永远不能执行,而外部线程在等待内部 sync {} 执行完成的条件下,导致外部的 sync {} 也无法执行完成。

总结:因为串行队列同时只能执行一个任务,就意味着无论如何,线程只能先执行完当前任务后,再继续执行下一个任务。而同步任务的特点是,会让线程等待它执行完成。那问题就来了,我 ( 线程 ) 既不可能先去执行它,又要等待它,结果是导致外部任务永远无法执行完成,而内部的任务也永远无法开启。

对于第二段代码 async { sync {} } 的死锁,原理是一样的,不要被它外部的 async {} 给迷惑了,内部的 sync {} 同样会阻塞它的线程执行,阻塞的结果就是外部的 async {} 无法执行完成,内部的 sync {} 也永远无法开启。

至于串行队列另外两种任务的嵌套结构 sync { async {} }async { async } ,例如:

queue.sync {
    print("task-1")
    queue.async {
        (0..<10).forEach {
            print("task-2: \($0) \(Thread.current)")
            Thread.sleep(forTimeInterval: 0.5)
        }
    }
    print("task-1 - end")
}
/**
 1
 task-1 - end
 task-2: 0 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
 task-2: 1 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
 task-2: 2 ... 9
*/

queue.sync {
    print("task-1")
    queue.async {
        (0..<10).forEach {
            print("task-2: \($0) \(Thread.current)")
            Thread.sleep(forTimeInterval: 0.5)
        }
    }
    print("task-1 - end")
}
/**
 1
 task-1 - end
 task-2: 0 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
 task-2: 1 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
 task-2: 2 ... 9
*/
复制代码

虽然已经不再死锁,但执行的顺序稍有不同,可以看到,程序是先把外部任务执行完后,再去执行内部任务。这是因为,内部的 async {} 已经不再阻塞当前线程,又因为串行队列只能先把当前任务执行完后,再去执行下一个任务,那自然而然就是先把外部任务执行完后,再接着去执行内部的 async {} 任务了。

示例5 - DispatchQueue.main 特殊串行主队列

前面说过,async 中的任务都会在其他线程执行,那对于主队列中的 async 呢?在项目中我们经常调用的 DispatchQueue.main.asyncAfter(deadline:) 难道是在其他线程执行吗?其实不是的,如果是 DispatchQueue.main 自己的队列,那么即使是 async ,也会在主线程执行,由于主队列本身是串行队列,也是同时只能执行一个任务,所以是,它会在处理完当前任务后,再去处理 async 中的任务,例如:

// 实际上相当于在 DispatchQueue.main.sync {} 中执行
print("1")

DispatchQueue.main.async {
    (0..<10).forEach { 
        print("async\($0) \(Thread.current)") 
        Thread.sleep(forTimeInterval: 0.2)
    }
}

print("3")

/**
 1
 3
 async0 <NSThread: 0x6000007928c0>{number = 1, name = main}
 async1 <NSThread: 0x6000007928c0>{number = 1, name = main}
 async2 <NSThread: 0x6000007928c0>{number = 1, name = main}
 async3 ...9
 */
复制代码

虽然 async 不阻塞当前线程执行,但是由于都在一个队列上,DispatchQueue.main 只能先执行完当前任务后,再继续执行下一个任务 ( async ) 。

而如果在主线程调用 DispatchQueue.main.sync {} 又会如何呢?答案是:会死锁。其实原因很简单,因为整个主线程的代码就相当于放在一个大的 DispatchQueue.main.sync {} 任务中,这时候如果再调用 DispatchQueue.main.sync {} ,结果肯定是死锁。

还有一点需要留意,一定要在主线程执行和有关 UI 的操作,如果是在其他线程执行,例如:

queue.async {	// 并发队列
    customView.backgroundColor = UIColor.blue
}
复制代码

很可能就会接收到一个 Main Thread Checker: UI API called on a background thread: -[UIView setBackgroundColor:] 的崩溃报告,因此主线程也被称为 UI 线程。

示例6 - 在并发队列中执行同步 ( sync ) 任务

let queue = DispatchQueue(label: "serial.com", attributes: .concurrent)

queue.sync {
    (0..<10).forEach {
        print("task-1 \($0): \(Thread.current)")
        Thread.sleep(forTimeInterval: 0.2)
    }
}

print("main-1")

queue.sync {
    (0..<10).forEach {
        print("task-2 \($0): \(Thread.current)")
        Thread.sleep(forTimeInterval: 0.2)
    }
}

print("main-2")

/**
 task-1 0: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-1 1: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-1 2: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-1 3: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-1 4: <NSThread: 0x6000023968c0>{number = 1, name = main}
 main-1
 task-2 0: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-2 1: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-2 2: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-2 3: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-2 4: <NSThread: 0x6000023968c0>{number = 1, name = main}
 main-2
 */
复制代码

使用并发队列执行同步任务和在主线程执行操作并没有区别,因为 sync 会牢牢的将当前线程固定住,让线程等待它执行完成后才能继续执行其他操作。这里也能够看到,main-1main-2 分别等待 sync 执行结束后才能执行。

示例7 - 在并发队列中执行异步 ( async ) 任务

在线程将要执行到某个队列的 async 时,队列才会开始并发执行任务,线程不可能跨越当前正在执行的操作去启动任务。举个例子:

// 指定为创建并发队列 (.concurrent)
let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)

(0..<100).forEach {
    print("main-\($0)")
    Thread.sleep(forTimeInterval: 0.02)
}

queue.async { print("task-1", Thread.current) }
queue.async { print("task-2", Thread.current) }
queue.async { print("task-3", Thread.current) }
queue.async { print("task-4", Thread.current) }
queue.async { print("task-5", Thread.current) }
queue.async { print("task-6", Thread.current) }

print("main-end")

/**
 main-0
 main-1
 main-2 ...99
 task-2 <NSThread: 0x282e387c0>{number = 3, name = (null)}
 task-4 <NSThread: 0x282e387c0>{number = 3, name = (null)}
 task-5 <NSThread: 0x282e387c0>{number = 3, name = (null)}
 task-3 <NSThread: 0x282e38800>{number = 5, name = (null)}
 task-6 <NSThread: 0x282e387c0>{number = 3, name = (null)}
 print("main-end")
 task-1 <NSThread: 0x282e04b40>{number = 4, name = (null)}
*/
复制代码

因为主线程也是串行队列,程序将按照顺序执行,等到所有循环执行完成后,才会执行 queue.async ,由于是并发队列,所有任务都会同时执行,执行顺序并不固定,而最后的 main-end 可能安插在队列中某个任务完成前后的地方。

因为在执行 main-end 之前,任务已经被队列并发出去了。对于主线程来说,它完成打印 main-end 的时间是固定的,但是队列中并发任务的执行完成的时间并不固定 ( 执行任务会消耗时间 ) 。这时主线程并不会等待 async 的所有任务执行结束就会继续执行打印 main-end 的操作。

所以是,如果在执行 async 的某个时间内刚好到了主线程需要打印 main-end 的时间,就会执行打印 main-end 的操作,而 async 中还没有完成的任务将会继续执行,如图:

并发的时机

可以看到,循环操作结束后,队列才开始并发执行任务,打印 main-end 的操作在 queue.async 之后执行,但是由于队列执行任务需要时间,所以 main-end 有可能在 queue.async 执行完成之前执行。

对于一条线程来说,它的所有操作绝对按照固定顺序执行,不存在一条线程同时执行多个任务的情况。而我们的所谓并发,就是给每个任务开辟一条线程出来执行,等到有某个线程执行完后,就会复用这条线程去执行其他在队列中还没有开始执行的任务。

一条线程只负责执行它当前任务中的所有操作,至于其他线程被开启后 ( 前提是不要开启同样的线程 ) ,它们就在各自的线程上分别独立执行任务,互不影响。举个例子:

假设你要跑100米,当跑到50米的时候,就会有5个人跟你一起跑,跑到终点的时候,可能是你跑得比他们都快,也有可能是他们之中的任意人跑得比你快。

那你就可以想象成那 "5个人" 就是并发中的任务 ( 同时执行) ,而 "你" 就是当前线程。

示例8 - 并发队列的疑惑 - sync { sync {} }

那什么时候会开启同样的线程呢?也就是说,假设有一条线程 3 在执行,那么在这条线程 3 还没有执行完成的时候,就又有一条线程为 3 的任务开启了。这对于 async 任务来说,几乎不可能 ( 我说几乎是因为我不确定,按照我的猜测,应该不会出现这种情况 ) ,也就是说,想要开启同样的一条线程执行异步任务,必须要等到前面的线程执行完后,再用这条线程去执行其他任务。

但是对于 sync 任务来说,在 sync 还没执行完的时候,我可以在 sync {} 内部又开启一个 sync {} 任务,因为 sync {} 注定在主线程执行 ( async 任务无法指定在哪一条线程执行,而是由系统自动分配 ) ,这样一来,就有了在一条线程还没有执行完的时候,就又有一条同样的线程开启执行任务了。在串行队列中,我们已经知道,这样做会造成死锁,那在并发队列中又会如何呢?例如:

let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)
queue.sync {
    print("sync-start")
    queue.sync {
        (0..<5).forEach {
            print("task \($0): \(Thread.current)")
            Thread.sleep(forTimeInterval: 0.5)
        }
    }
    print("sync-end")
}

/**
 sync-start
 task 0: <NSThread: 0x600003b828c0>{number = 1, name = main}
 task 1: <NSThread: 0x600003b828c0>{number = 1, name = main}
 task 2: <NSThread: 0x600003b828c0>{number = 1, name = main}
 task 3: <NSThread: 0x600003b828c0>{number = 1, name = main}
 task 4: <NSThread: 0x600003b828c0>{number = 1, name = main}
 sync-end
 */
复制代码

我们已经看到结果,任务按照顺序执行,内部 sync 会阻塞外部 sync 我们也会清楚,问题是在外部的 sync {} 还没有执行完的时候,为什么内部的 sync 可以执行?

首先要了解最重要的一点,那就是,为什么在串行队列中内部的 sync {} 无法执行?最重要的原因在于串行队列同时只能执行一个任务,所以在它上一个任务 ( 外部 sync ) 还没有执行完成之前,它是不能执行下一个任务 ( 内部 sync ) 的。

而并发队列就不同了,并发队列可以同时执行多个任务。也就是说,内部的 sync 已经不用等待外部 sync 执行完成就可以执行了。但是由于是同步任务,所以还是会等待,等待内部 sync 执行完成后,外部的 sync 继续执行。

请注意这里的执行和上面所说的,不存在一条线程同时执行多个任务的情况并不矛盾。因为在执行内部 sync 时,外部线程就停止操作了 ( 其实是转去执行内部 sync 了 ) ,如果是在执行内部 sync 的同时,外部的 sync 还在继续执行操作,那才叫同时

因为 sync 都在一个线程 ( 主线程 ) 上,所以当你指定任务为 sync 时,主线程就知道接下来要去执行 sync 任务了,等执行完这个 sync 后再执行其他操作。例如,你可以把 sync 想象成是一个方法:

let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)

queue.sync {
    print("sync-start")
	queueSync()
    print("sync-end")
}

// 相当于之前的 queue.sync {}
func queueSync() {
    (0..<5).forEach {
        print("task \($0): \(Thread.current)")
        Thread.sleep(forTimeInterval: 0.5)
    }
}
复制代码

关于先进先出 (FIFO)

对串行队列来说,先进先出的意思很好理解,先进先出就是,先进去的一定先执行。当我们要执行一些任务时,这些任务就被存储在它的队列中,当线程进入到任务代码块时,就一定会先把这个任务执行完,再将任务出列,等这个任务出列后,线程才能继续去执行下一个任务。

那对于并发队列也是一样,当不同的线程同时进入到任务代码块时,就一定会先把这些任务执行完,再将这些任务出列,然后这些线程才能继续去执行其他任务。

示例9 - 关于并发的个数和线程性能

let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)
(0..<100).forEach { i in
    queue.async { print("\(i) \(Thread.current)") }
}
复制代码

会怎么样?答案是不会怎么样,只是会开启很多线程来执行这些异步任务。前面说过,每一个异步任务都是在不同的线程上执行的,那如果同时执行很多异步任务的话,像我们这里,同时开启 100 个异步任务,难道就系统就开辟 100 个线程来分别执行吗?也不是没有可能,这取决于你的 CPU,如果在 App 运行时,系统所能承载的最大线程个数为 10,那就会开辟这 10 条线程来重复执行任务,一次执行 10 个异步任务。

如果开辟的线程上限,那么剩下的那些任务就暂时无法执行,只能等到前面那些异步任务的线程执行完后,再去执行后面的异步任务。

总之一句话就是重复利用,先执行完的去执行还没有开始执行的,如果开辟的线程超出限制,那后面的任务就要等待前面的线程执行完才能执行。

但是如果开辟很多线程的话,会不会对我们的应用程序有负的影响?答案是一定的,开辟一条线程就要消耗一定的内存空间和系统资源,如果同时存在很多线程的话,那本身留给应用程序的内存就少得可怜,应用程序在运行时就会很卡,所以并不是线程开得越多越好,需要开发者自己平衡。

示例10 - DispatchQueue.global(_:) 全局并发队列

除了串行主队列外,系统还为我们创建了一个全局的并发队列 ( DispatchQueue.global() ) ,如果不想自己创建并发队列,那就用系统的 ( 我们一般也是用系统的 ) 。

DispatchQueue.global().async {
    print("global async start \(Thread.current)")
    DispatchQueue.global().sync {
        (0..<5).forEach {
            print("roop\($0) \(Thread.current)")
            Thread.sleep(forTimeInterval: 0.2)
        }
    }
    print("global async end \(Thread.current)")
}

/**
 global async start <NSThread: 0x600002085300>{number = 3, name = (null)}
 roop0 <NSThread: 0x600002085300>{number = 3, name = (null)}
 roop1 <NSThread: 0x600002085300>{number = 3, name = (null)}
 roop2 <NSThread: 0x600002085300>{number = 3, name = (null)}
 roop3 <NSThread: 0x600002085300>{number = 3, name = (null)}
 roop4 <NSThread: 0x600002085300>{number = 3, name = (null)}
 global async end <NSThread: 0x600002085300>{number = 3, name = (null)}
 */
复制代码

和主队列一样,它的特殊之处在于,即使是用 sync ,任务也会在其他线程执行,至于它在哪一条线程执行,我猜测是它一定会让执行外部 async 的这条线程来执行,因为 sync 就是会让线程暂停执行后续操作,等到 sync 执行完后再接着执行,也就是说,在这种情况下,它只能顺序执行,那似乎只要一条线程就足够了,没有必要再开辟新线程来执行内部的 sync

另外,全局并发队列只有一个,并不是调用一次系统就创建一个,经过测试,它们是相等的:

let queue1 = DispatchQueue.global()
let queue2 = DispatchQueue.global()

if queue1 == queue2 { print("相等") }

// Prints "相等"
复制代码

总结

在前面的示例中,有关概念都是跟随示例引申出来的,讲得不是那么统一,在这里就总结一下。

  • 队列

    • 串行队列 在串行队列中执行任务时,任务按固定顺序执行,只能执行完一个任务后,再继续执行下一个任务 ( 这意味着串行队列同时只能执行一个任务 ) 。

    • 并发队列

      并发队列可以同时执行多个任务,任务并不一定按顺序执行,先执行哪几个任务由系统自动分配决定,等到有某个任务执行完后,就将这个任务出列,然后线程才能继续去执行其他任务。

  • 任务

    • 同步任务

      不管是串行还是异步队列,只要是同步任务,就在主线程执行 ( DispatchQueue.global().sync 例外 ) 。

      同步任务会阻塞当前线程,让当前线程只能等待它执行完毕后才能执行。

      在串行队列中,任务嵌套了 sync {} 的话会导致死锁。

    • 异步任务

      不论是串行还是异步队列,只要是异步任务,就在其他线程执行 ( DispatchQueue.main.sync 例外 ) ,不同的是串行队列在执行异步任务时,只会开辟一条线程,而并发队列在执行异步任务时,可以开辟多条线程。

      异步任务不会阻塞当前线程,线程不用等待异步任务执行完成就可以继续执行其他任务/操作。

      异步任务不会产生死锁。

文章分类
iOS
文章标签