iOS - 多线程分析之 DispatchQueue Ⅰ

栏目: IOS · 发布时间: 5年前

内容简介:Dispatch ( 全称 Grand Central Dispatch,简称 GCD ) 是一套由 Apple 编写以提供让代码以多核并发的方式执行应用程序的框架。在使用它之前,我们得先了解一下基本概念,我会先简单介绍,后面再根据讲解的内容逐步详细介绍,目的是为了方便读者融入。

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

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

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

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

同步和异步执行

iOS - 多线程分析之 DispatchQueue Ⅰ

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

阅读指南:

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

串行和并发队列

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

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

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

iOS - 多线程分析之 DispatchQueue Ⅰ

示例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 中还没有完成的任务将会继续执行,如图:

iOS - 多线程分析之 DispatchQueue Ⅰ

可以看到,循环操作结束后,队列才开始并发执行任务,打印 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 例外 ) ,不同的是 串行队列在执行异步任务时,只会开辟一条线程,而并发队列在执行异步任务时,可以开辟多条线程

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

      异步任务不会产生死锁。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Development Recipes

Web Development Recipes

Brian P. Hogan、Chris Warren、Mike Weber、Chris Johnson、Aaron Godin / Pragmatic Bookshelf / 2012-1-22 / USD 35.00

You'll see a full spectrum of cutting-edge web development techniques, from UI and eye candy recipes to solutions for data analysis, testing, and web hosting. Make buttons and content stand out with s......一起来看看 《Web Development Recipes》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

SHA 加密
SHA 加密

SHA 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试