iOS 多线程相关
概念
在开始多线程之前,我们先来了解几个比较容易混淆的概念。
线程与进程
一个进程,可以拥有一个或多个线程。
runloop与线程
https://www.jianshu.com/p/9a46e6762fca
并发与并行
并发指多个任务交替占用CPU,并行指多个CPU同时执行多个任务。好比在火车站买票,并发是指一个窗口有多人排队买票,并行是指多个窗口有多人排队买票。
同步和异步
同步指在执行一个函数时,如果这个函数没有执行完毕,那么下一个函数便不能执行。异步指在执行一个函数时,不必等到这个函数执行完毕,便可开始执行下一个函数。
四种方式
iOS目前有四种多线程方式:
- Pthreads
- NSThread
- GCD
- NSOperation & NSOperationQueue
Pthreads
这个方法不必多说,大家了解一下就好,按照百度百科里的解释:
POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。
简单的说,这是一套在很多操作系统上都适用的多线程API,所以移植性很强(然并卵)。虽然它在iOS系统中是适用的,但它是基于c语言的框架,用起来是相当的酸爽(需要程序员自己管理线程的生命周期),下面可以来体验一下:
- (void)threads {
// 定义一个pthread_t类型的变量
pthread_t thread;
// 创建一个线程,并自动运行
pthread_create(&thread, NULL, run, NULL);
// 设置该线程的状态为detached,该线程结束后会自动释放所有资源
pthread_detach(thread);
}
void *run(void *data) { // 新线程调用的方法,里面有需要执行的任务
NSLog(@"%@", [NSThread currentThread]);
return NULL;
}
打印结果:
2019-05-30 22:25:55.516777+0800 studyDemo[46180:3419195] <NSThread: 0x6000014ffac0>{number = 3, name = (null)}
NSThread
这套方案是经过苹果封装后的,并且完全面向对象的。所以可以直接操控线程对象,非常直观和方便。但是它的生命周期还是需要手动管理,所以使用较少。比如[NSThread currentThread],它可以获取当前线程类,从而可以知道当前线程的各种属性,用于调试非常方便。下面来看代码:
- (void)threads_NSThread {
// 创建
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadLog) object:nil];
// 启动
[thread start];
// 创建并启动
[NSThread detachNewThreadSelector:@selector(threadLog) toTarget:self withObject:nil];
}
- (void)threadLog {
NSLog(@"%@", [NSThread currentThread]);
}
打印结果:
2019-05-30 22:39:37.926337+0800 studyDemo[46421:3427904] <NSThread: 0x6000029c10c0>{number = 3, name = (null)}
GCD
同步
let queue = DispatchQueue(label: "com.ffib.blog")
queue.sync {
for i in 0..<5 {
print(i)
}
}
for i in 10..<15 {
print(i)
}
output:
0
1
2
3
4
10
11
12
13
14
从结果可以看出队列同步操作时,当程序在进行队列任务时,主线程的操作并不会被执行,这是由于当程序在执行同步操作时,会阻塞线程,所以需要等待队列任务执行完毕,程序才可以继续执行。
异步
let queue = DispatchQueue(label: "com.ffib.blog")
queue.async {
for i in 0..<5 {
print(i)
}
}
for i in 10..<15 {
print(i)
}
output:
10
11
12
13
14
0
1
2
3
4
从结果可以看出队列异步操作时,当程序在执行队列任务时,不必等待队列任务开始执行,便可执行主线程的操作。与同步执行相比,异步队列并不会阻塞主线程,当主线程空闲时,便可执行别的任务。
QoS 优先级
在实际开发中,我们需要对任务分类,比如UI的显示和交互操作等,属于优先级比较高的,有些不着急操作的,比如缓存操作、用户习惯收集等,相对来说优先级比较低。
在GCD中,我们使用队列和优先级划分任务,以达到更好的用户体验,选择合适的优先级,可以更好的分配CPU的资源。
GCD内采用DispatchQoS结构体,如果没有指定QoS,会使用default。
以下等级由高到低。
public struct DispatchQoS : Equatable {
public static let userInteractive: DispatchQoS //用户交互级别,需要在极快时间内完成的,例如UI的显示
public static let userInitiated: DispatchQoS //用户发起,需要在很快时间内完成的,例如用户的点击事件、以及用户的手势
。
public static let `default`: DispatchQoS //系统默认的优先级,与主线程优先级一致
public static let utility: DispatchQoS //实用级别,不需要很快完成的任务
public static let background: DispatchQoS //用户无法感知,比较耗时的一些操作
public static let unspecified: DispatchQoS
}
以下通过两个例子来具体看一下优先级的使用。
相同优先级
let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)
queue1.async {
for i in 5..<10 {
print(i)
}
}
queue2.async {
for i in 0..<5 {
print(i)
}
}
output:
0
5
1
6
2
7
3
8
4
9
从结果可见,优先级相同时,两个队列是交替执行的。
不同优先级
let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .default)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)
queue1.async {
for i in 0..<5 {
print(i)
}
}
queue2.async {
for i in 5..<10 {
print(i)
}
}
output:
0
1
2
3
4
5
6
7
8
9
从结果可见,CPU会把更多的资源优先分配给优先级高的队列,等到CPU空闲之后才会分配资源给优先级低的队列。
主队列默认使用拥有最高优先级,即userInteractive,所以慎用这一优先级,否则极有可能会影响用户体验。
一些不需要用户感知的操作,例如缓存等,使用utility即可
串行队列
在创建队列时,不指定队列类型时,默认为串行队列。
let queue = DispatchQueue(label: "com.ffib.blog.initiallyInactive.queue", qos: .utility)
queue.async {
for i in 0..<5 {
print(i)
}
}
queue.async {
for i in 5..<10 {
print(i)
}
}
queue.async {
for i in 10..<15 {
print(i)
}
}
output:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
从结果可见队列执行结果,是按任务添加的顺序,依次执行。
并行队列
let queue = DispatchQueue(label: "com.ffib.blog.concurrent.queue", qos: .utility, attributes: .concurrent)
queue.async {
for i in 0..<5 {
print(i)
}
}
queue.async {
for i in 5..<10 {
print(i)
}
}
queue.async {
for i in 10..<15 {
print(i)
}
}
output:
5
0
10
1
2
3
11
4
6
12
7
13
8
14
9
从结果可见,所有任务是以并行的状态执行的。另外在设置attributes参数时,参数还有另一个枚举值initiallyInactive,表示的任务不会自动执行,需要程序员去手动触发(queue.activate()
)。如果不设置,默认是添加完任务后,自动执行。
添加initiallyInactive这一属性带来了,更多的灵活性,可以自由的决定执行的时机。
再来看看并行队列如何设置这一枚举值。
let queue = DispatchQueue.init(label: "Qos5", qos: .utility, attributes: [.concurrent, .initiallyInactive])
延迟执行
GCD提供了任务延时执行的方法,通过对已创建的队列,调用延时任务的函数即可。其中时间以DispatchTimeInterval设置,GCD内跟时间参数有关系的参数都是通过这一枚举来设置。
public enum DispatchTimeInterval : Equatable {
case seconds(Int) //秒
case milliseconds(Int) //毫秒
case microseconds(Int) //微妙
case nanoseconds(Int) //纳秒
case never
}
在设置调用函数时,asyncAfter有两个及其相同的方法,不同的地方在于参数名有所不同,参照Stack Overflow的解释。
wallDeadline 和 deadline,当系统睡眠后,wallDeadline会继续,但是deadline会被挂起。例如:设置参数为60分钟,当系统睡眠50分钟,wallDeadline会在系统醒来之后10分钟执行,而deadline会在系统醒来之后60分钟执行。
let queue = DispatchQueue(label: "com.ffib.blog.after.queue")
let time = DispatchTimeInterval.seconds(5)
queue.asyncAfter(wallDeadline: .now() + time) {
print("wall dead line done")
}
queue.asyncAfter(deadline: .now() + time) {
print("dead line done")
}
DispatchQueue.main.asyncAfter(deadline: .now() + Double(NSEC_PER_SEC * 1) / Double(NSEC_PER_SEC)) {
print("主线程 dead line done")
}
dispatch_group (组)
如果想等到所有的队列的任务执行完毕再进行某些操作时,可以使用DispatchGroup来完成。
let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)
queue1.async(group: group) {
for i in 0..<5 {
print(i)
}
}
queue2.async(group: group) {
for i in 6..<10 {
print(i)
}
}
//group内所有线程的任务执行完毕
group.notify(queue: DispatchQueue.main) {
print("done")
}
output:
5
0
6
1
7
2
8
3
9
4
done
如果想等待某一队列先执行完毕再执行其他队列可以使用wait
group.wait()
// 上方的output会变为:0~9 done,因为wait会同步等待先前提交的工作完成。(类似同步执行)
为防止队列执行任务时出现阻塞,导致线程锁死,可以设置超时时间。
// 同步等待先前提交的工作完成,如果在指定的超时时间之前工作未完成,则返回。
group.wait(timeout: <#T##DispatchTime#>)
// 作用同上,DispatchTime为主板时间CPU时钟计时, DispatchWallTime为实际时间即系统时间
group.wait(wallTimeout: <#T##DispatchWallTime#>)
DispatchWorkItem
Swift3新增的api,可以通过此api设置队列执行的任务。先看看简单应用吧。通过DispatchWorkItem初始化闭包。
let workItem = DispatchWorkItem {
for i in 0..<10 {
print(i)
}
}
调用一共分两种情况,第一种是通过调用perform(),自动响应闭包。
DispatchQueue.global().async {
workItem.perform()
}
第二种是作为参数传给async方法。
DispatchQueue.global().async(execute: workItem)
接下来我们来看看DispatchWorkItem的内部都有些什么方法和属性。
init(qos: DispatchQoS = default, flags: DispatchWorkItemFlags = default,
block: @escaping () -> Void)
从初始化方法开始,DispatchWorkItem也可以设置优先级,另外还有个参数DispatchWorkItemFlags,来看看DispatchWorkItemFlags的内部组成。
public struct DispatchWorkItemFlags : OptionSet, RawRepresentable {
public static let barrier: DispatchWorkItemFlags
public static let detached: DispatchWorkItemFlags
public static let assignCurrentContext: DispatchWorkItemFlags
public static let noQoS: DispatchWorkItemFlags
public static let inheritQoS: DispatchWorkItemFlags
public static let enforceQoS: DispatchWorkItemFlags
}
DispatchWorkItemFlags主要分为两部分:
- 覆盖
- noQoS 没有优先级
- inheritQoS 继承Queue的优先级
- enforceQoS 覆盖Queue的优先级
- 执行情况
- barrier
- detached
- assignCurrentContext
执行情况会在下文会具体描述,先在这留个坑。
先来看看设置优先级,会对任务执行有什么影响。
let queue1 = DispatchQueue(label: "com.ffib.blog.workItem1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.workItem2", qos: .userInitiated)
let workItem1 = DispatchWorkItem(qos: .userInitiated) {
for i in 0..<5 {
print(i)
}
}
let workItem2 = DispatchWorkItem(qos: .utility) {
for i in 5..<10 {
print(i)
}
}
queue1.async(execute: workItem1)
queue2.async(execute: workItem2)
output:
5
6
7
8
9
0
1
2
3
4
由结果可见即使设置了DispatchWorkItem
仅仅只设置了优先级并不会对任务执行顺序有任何影响。也就是仍然按照queue的优先级执行。
接下来,再来设置DispatchWorkItemFlags
试试
let queue1 = DispatchQueue(label: "com.ffib.blog.workItem1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.workItem2", qos: .userInitiated)
let workItem1 = DispatchWorkItem(qos: .userInitiated, flags: .enforceQoS) {
for i in 0..<5 {
print(i)
}
}
let workItem2 = DispatchWorkItem {
for i in 5..<10 {
print(i)
}
}
queue1.async(execute: workItem1)
queue2.async(execute: workItem2)
output:
5
0
6
1
7
2
8
3
9
4
设置enforceQoS,使优先级强制覆盖queue的优先级,所以两个队列呈交替执行状态,变为同一优先级。
DispatchWorkItem
也有wait
和notify
方法,和DispatchGroup
用法相同。wait会等待这个workItem执行完毕。会阻塞当前线程。也可以使用cancel()提前取消任务。
// 执行结束通过notify提示主队列
workItem.notify(queue: DispatchQueue.main) {
print("value = ", value)
}
// wait会等待这个workItem执行完毕。会阻塞当前线程。workItem3会先执行完,之后再执行workItem2
queue12.async(execute: workItem4)
queue13.async(execute: workItem3)
workItem3.wait()
dispatch_once (单次)
一般用于单例
// swift
class Tool: NSObject {
static let share = Tool()
}
// OC
// Tool.h
@interface Tool : NSObject
+ (instancetype)sharedInstance;
@end
// Tool.m
@implementation Tool
+ (instancetype)sharedInstance {
static Tool *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
@end
DispatchSemaphore(信号量)
如果你想同步执行一个异步队列任务,可以使用信号量。
wait()会使信号量减一,如果信号量大于1则会返回.success,否则返回timeout(超时),也可以设置超时时间。
func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult
func wait(timeout: DispatchTime) -> DispatchTimeoutResult
signal()会使信号量加一,返回当前信号量。
print("DispatchSemaphore: 开始")
let semaphore = DispatchSemaphore.init(value: 1)
let queue = DispatchQueue.init(label: "semaphore0", qos: .utility)
for i in 0 ..< 5 {
print("wait: \(i)")
if semaphore.wait(timeout: .distantFuture) == .success {
queue.async {
sleep(2)
print("semaphore: \(semaphore.signal())、\(i)")
}
}
}
print("DispatchSemaphore: 结束")
output:
DispatchSemaphore: 开始
wait: 0
wait: 1
semaphore: 1、0
wait: 2
semaphore: 1、1
wait: 3
semaphore: 1、2
wait: 4
semaphore: 1、3
DispatchSemaphore: 结束
semaphore: 0、4
我们来看下for循环里都发生了什么。第一遍循环遇到wait时,此时信号量为1,大于0,所以if判断为true,进行sleep和打印操作;当第二遍循环遇到wait时,发现信号量为0,此时就会锁死线程,直到上一遍循环的操作完成,调用signal()方法,信号量加一,才会继续执行操作,循环以上操作。
DispatchSemaphore还有另外一个用法,可以限制队列的最大并发量,通过前面所说的wait()信号量减一,signal()信号量加一,来完成此操作,正如上文所述例子,其实达到的效果就是最大并发量为一。
如果使用过NSOperationQueue的同学,应该知道maxConcurrentOperationCount,效果是类似的。
DispatchWorkItemFlags
barrier
可以理解为隔离,在读取时,可以异步访问,但是如果突然出现了异步写入操作,我们想要达到的效果是在进行写入操作的时候,使读取操作暂停,直到写入操作结束,再继续进行读取操作,以保证读取操作获取的是最新内容。
预期结果是:在写入操作之前,读取到的内容是a;在写入操作之后,读取到的内容是b(即写入的内容)。
先看看不使用barrier的结果。
print("DispatchWorkItemFlags: 开始")
var testStr = "a"
let queue1 = DispatchQueue.init(label: "flags", attributes: .concurrent)
let readWorkItem = DispatchWorkItem.init {
sleep(1)
print(testStr)
}
let writeWorkItem = DispatchWorkItem.init {
sleep(3)
testStr = "b"
print("write")
}
queue1.async(execute: readWorkItem)
queue1.async(execute: writeWorkItem)
queue1.async(execute: readWorkItem)
print("DispatchWorkItemFlags: 结束")
output:
DispatchWorkItemFlags: 开始
DispatchWorkItemFlags: 结束
a
a
write
结果不是我们想要的。再来看看加了barrier之后的效果。
// 将上题writeWorkItem修改初始化方式
let writeWorkItem = DispatchWorkItem.init(flags: [.barrier]) {
// 里面内容同上,不变
}
output:
DispatchWorkItemFlags: 开始
DispatchWorkItemFlags: 结束
a
write
b
结果符合预期的想法,barrier主要用于读写隔离,以保证写入的时候,不被读取。
dispatch_barrier_async (栅栏)
释义
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
dispatch_barrier_async一般叫做“栅栏函数”,它就好像栅栏一样可以将多个操作分隔开,在它前面追加的操作先执行,在它后面追加的操作后执行。
栅栏函数也可以执行队列上的操作(参数列表中有queue和block),也有对应的 dispatch_barrier_sync 函数。
示例:
- (void)testBarrierAsync
{
//创建一个并行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.gcd.barrier.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//并行操作
void (^blk1)() = ^{
NSLog(@"1");
};
void (^blk2)() = ^{
NSLog(@"2");
};
void (^blk3)() = ^{
NSLog(@"3");
};
void (^blk4)() = ^{
NSLog(@"4");
};
void (^blk5)() = ^{
NSLog(@"5");
};
void (^blk6)() = ^{
NSLog(@"6");
};
//栅栏函数执行操作
void (^barrierBlk)() = ^{
NSLog(@"Barrier!");
};
//执行所有操作
dispatch_async(concurrentQueue, blk1);
dispatch_async(concurrentQueue, blk2);
dispatch_async(concurrentQueue, blk3);
dispatch_barrier_async(concurrentQueue, barrierBlk);
dispatch_async(concurrentQueue, blk4);
dispatch_async(concurrentQueue, blk5);
dispatch_async(concurrentQueue, blk6);
}
方法执行结果:
2
1
3
Barrier!
5
4
6
分析:
栅栏函数之前和之后的操作执行顺序都不固定,但是前面三个必然先执行,然后再执行栅栏函数中的操作,最后执行后面的三个。
注意:
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block)
栅栏函数中传入的参数队列必须是由 dispatch_queue_create 方法创建的队列,否则,与dispatch_async无异,起不到“栅栏”的作用了,对于dispatch_barrier_sync也是同理。