主要涉及到的概念
- 任务:放在block中的代码,执行方式有 同步执行(sync) 和 异步执行(async)
- 队列: 串行队列(serial) 和 并发队列(concurrent)
- 主队列:本质是一个串行队列
- 全局队列:本质是一个并发队列
知识点:
- 主队列的任务一定在主线程执行,主线程可以执行非主队列的任务
- 同步执行不开辟子线程,在当前线程执行
- 异步执行具有开辟子线程的能力,但是不一定会开辟,后面会有例子
- 同步执行串行队列按照顺序串行执行
- 同步执行并发队列依然是按照顺序串行执行
- 异步执行串行队列会开辟一个子线程,按顺序执行队列任务
- 异步执行并发队列具备开辟多个子线程的能力
任务的执行方式有两种,队列有四种,共有8种组合,分别为:
同步执行+串行队列
dispatch_queue_t queueSerial = dispatch_queue_create("com.serial", DISPATCH_QUEUE_SERIAL);
void(^task)(void) = ^{
NSLog(@"task--%@",[NSThread currentThread]);
};
void(^task1)(void) = ^{
NSLog(@"task1--%@",[NSThread currentThread]);
};
dispatch_sync(queueSerial, task);
dispatch_sync(queueSerial, task1);
NSLog(@"end");
复制代码
当前是在主线程运行,同步执行会阻塞当前线程(主线程),所以end会在执行完task
和task1
之后输出,
dispatch_sync(queueSerial, task);
复制代码
- 同步执行
dispatch_sync
阻塞当前线程(主线程) - 向串行队列
queueSerial
中插入任务task
- 在当前线程(主线程)执行
task
任务 task
任务执行完成解除对当前线程(主线程)的阻塞
dispatch_sync(queueSerial, task1); //同理
复制代码
- 同步执行
dispatch_sync
阻塞当前线程(主线程) - 向串行队列
queueSerial
中插入任务task1
- 在当前线程(主线程)执行
task1
任务 task1
任务执行完成解除对当前线程(主线程)的阻塞
所以执行顺序为task
->task1
->end
同步执行+并发队列
dispatch_queue_t queueConcurrent = dispatch_queue_create("com.conc", DISPATCH_QUEUE_CONCURRENT);
void(^task)(void) = ^{
NSLog(@"task--%@",[NSThread currentThread]);
};
void(^task1)(void) = ^{
NSLog(@"task1--%@",[NSThread currentThread]);
};
dispatch_sync(queueConcurrent, task);
dispatch_sync(queueConcurrent, task1);
NSLog(@"end");
复制代码
在当前线程同步执行无论是串行队列还是并发队列任务都是按照顺序执行,其结果和 同步执行+串行队列 一致
同步执行全局队列
//第一个参数是队列优先级,第二个参数是保留值默认传0
dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);
void(^task)(void) = ^{
NSLog(@"task--%@",[NSThread currentThread]);
};
void(^task1)(void) = ^{
NSLog(@"task1--%@",[NSThread currentThread]);
};
dispatch_sync(queueGlobal, task);
dispatch_sync(queueGlobal, task1);
NSLog(@"end");
复制代码
全局队列本质是并发队列,其结果和 同步执行+并发队列 一致
同步执行主队列
dispatch_queue_t queueMain = dispatch_get_main_queue();
void(^task)(void) = ^{
NSLog(@"task--%@",[NSThread currentThread]);
};
void(^task1)(void) = ^{
NSLog(@"task1--%@",[NSThread currentThread]);
};
dispatch_sync(queueMain, task); //我也是一个任务 记为 sync1
dispatch_sync(queueMain, task1);
NSLog(@"end");
复制代码
这种情况比较特殊,会引起死锁,其原因是同步执行语句dispatch_sync(queueMain, task);
本身也是一个任务,我们记为 sync1
,sync1
任务是在当前线程(主线程)同步执行的,sync1
任务是向主队列中添加一个任务task
并在当前线程(主线程)同步执行任务task
,也就是说sync1
任务执行完成的前提是task
任务执行完成。
因为task
任务是在sync1
任务之后插入主队列的,所以主线程要先执行完sync1
任务才会去执行task
任务,而sync1
任务执行完成的前提是task
任务执行完成,所以就陷入了死锁状态,造成崩溃。
类似的
dispatch_queue_t queueSerial = dispatch_queue_create("com.serial", DISPATCH_QUEUE_SERIAL);
void(^task1)(void) = ^{
NSLog(@"死锁了吗");
};
void(^task2)(void) = ^{
dispatch_sync(queueSerial, task1);
};
//开辟子线程,并向串行队列queueSerial中添加任务 task2
dispatch_async(queueSerial, task2);
复制代码
dispatch_async(queueSerial, task2);
本身也是一个任务记为sync1
,但不同的是sync1
是在当前线程(主线程)执行,sync1
任务执行结果是向串行队列queueSerial
中添加任务task2
并开辟子线程执行task2
,任务task2
是向串行队列queueSerial
中添加任务task1
并在当前线程同步执行任务task1
,也就是说task2
任务执行完成的前提是task1
任务执行完成,因为task2
任务比task1
任务先添加到串行队列queueSerial
,所以任务task2
执行完成之后才会去执行tasi1
,
注意:异步执行+串行队列只会开辟一条子线程
异步执行串行队列
dispatch_queue_t queueSerial = dispatch_queue_create("com.serial", DISPATCH_QUEUE_SERIAL);
void(^task)(void) = ^{
NSLog(@"task--%@",[NSThread currentThread]);
};
void(^task1)(void) = ^{
NSLog(@"task1--%@",[NSThread currentThread]);
};
dispatch_async(queueSerial, task); //我也是一个任务 记为 sync1
dispatch_async(queueSerial, task1);
NSLog(@"end");
复制代码
将task
和task1
分别添加到串行队列queueSerial
,开辟一条子线程顺序执行队列queueSerial
中的任务,因为end是在主线程输出的,所以输出顺序为end
->task
->task1
,因为异步执行+串行队列只开辟一条子线程,所以task
和task1
在同一个线程中执行
异步执行并发队列
这个时候理论上可以开辟多个子线程了
dispatch_queue_t queueConcurrent = dispatch_queue_create("com.conc", DISPATCH_QUEUE_CONCURRENT);
void(^task)(void) = ^{
NSLog(@"task--%@",[NSThread currentThread]);
};
void(^task1)(void) = ^{
NSLog(@"task1--%@",[NSThread currentThread]);
};
dispatch_async(queueConcurrent, task);
dispatch_async(queueConcurrent, task1);
NSLog(@"end");
复制代码
因为task
和task1
有可能不是在同一个字线程执行的,两个任务不一定谁先结束执行,所以不能确定task
和task1
谁先输出,因为end是在主线程输出的,所以先输出end
异步执行主队列
dispatch_queue_t queueMain = dispatch_get_main_queue();
void(^task)(void) = ^{
NSLog(@"task--%@",[NSThread currentThread]);
};
void(^task1)(void) = ^{
NSLog(@"task1--%@",[NSThread currentThread]);
};
dispatch_async(queueMain, task);
dispatch_async(queueMain, task1);
复制代码
主队列本质上也是一个串行队列,所以按照顺序执行task
->task1
,另外祝队列任务只能在主线程执行,所以task
和task1
都在主线程执行
异步执行全局队列
dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);
void(^task)(void) = ^{
NSLog(@"task--%@",[NSThread currentThread]);
};
void(^task1)(void) = ^{
NSLog(@"task1--%@",[NSThread currentThread]);
};
dispatch_async(queueGlobal, task);
dispatch_async(queueGlobal, task1);
复制代码
全局队列是一个并发队列,所以有能力开辟多个子线程,当然也可能在一个子线程中去执行
线程间通讯
我们需要将一些耗时任务放在子线程,执行完毕之后再回到主线程刷新页面
dispatch_queue_t queueMain = dispatch_get_main_queue();
dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);
dispatch_async(queueGlobal, ^{
sleep(3);//模拟耗时操作
dispatch_async(queueMain, ^{
NSLog(@"主线程");
});
});
复制代码
GCD 栅栏方法:dispatch_barrier_async
在《程序员的自我修养:链接、装载与库。》一书的过度优化部分有这么一段话
“CPU的乱序执行能力让我们对多线程的安全保障的努力变得异常困难。因此要保证线程安全,阻止CPU换序是必需的。遗憾的是,现在并不存在可移植的阻止换序的方法。通常情况下是调用CPU提供的一条指令,这条指令常常被称为barrier。一条barrier指令会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。换句话说,barrier指令的作用类似于一个拦水坝,阻止换序“穿透”这个大坝。”
摘录来自: 俞甲子 石凡 潘爱民. “程序员的自我修养:链接、装载与库。” Apple Books.
为了保证某些操作的原子性,CUP提供了barrier
指令,用来保证在barrier
指令之前的指令执行完成之后才会执行barrier
之后的指令,dispatch_barrier_async
的意思大体也是如此,在异步执行并发队列中保证先执行完dispatch_barrier_async
之前的任务,然后再执行dispatch_barrier_async
中的任务,其次执行dispatch_barrier_async
之后的任务
dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);
dispatch_async(queueGlobal, ^{
NSLog(@"1--%@",[NSThread currentThread]);
});
dispatch_async(queueGlobal, ^{
sleep(2);
NSLog(@"2--%@",[NSThread currentThread]);
});
dispatch_barrier_async(queueGlobal, ^{
NSLog(@"barrier--%@",[NSThread currentThread]);
});
dispatch_async(queueGlobal, ^{
NSLog(@"3--%@",[NSThread currentThread]);
});
dispatch_async(queueGlobal, ^{
NSLog(@"4--%@",[NSThread currentThread]);
});
复制代码
全局队列是一个并发队列,盲猜这里应该先打印出来1
和2
,然后执行barrier
,然后随机打出3
和4
,但是实际上呢
barrier
似乎跟dispatch_async
一样,并没有起到什么作用,查了一番资料终于在官方文档找到正解
dispatch_barrier_async
如果自己通过dispatch_queue_create
创建并发队列没问题,如果是一个串行队列或者全局并发队列那么和dispatch_async
效果一样一样的,我们换成自己创建的并发队列
dispatch_queue_t queueConcurrent = dispatch_queue_create("com.conc", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queueConcurrent, ^{
NSLog(@"1--%@",[NSThread currentThread]);
});
dispatch_async(queueConcurrent, ^{
sleep(2);
NSLog(@"2--%@",[NSThread currentThread]);
});
dispatch_barrier_async(queueConcurrent, ^{
NSLog(@"barrier--%@",[NSThread currentThread]);
});
dispatch_async(queueConcurrent, ^{
NSLog(@"3--%@",[NSThread currentThread]);
});
dispatch_async(queueConcurrent, ^{
NSLog(@"4--%@",[NSThread currentThread]);
});
NSLog(@"end");
复制代码
从结果看到dispatch_barrier_async
并没有阻塞当前线程,而只是阻塞了当前异步队列其他任务的执行,官方文档也证实了这一点
dispatch_barrier_sync
我们先去官方文档看一下dispatch_barrier_sync
一方面dispatch_barrier_sync
会阻塞当前线程,另一方面可能造成死锁deadlock
,我们验证一下
dispatch_queue_t queueConcurrent = dispatch_queue_create("com.conc", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queueConcurrent, ^{
sleep(2);
NSLog(@"1--%@",[NSThread currentThread]);
});
dispatch_async(queueConcurrent, ^{
NSLog(@"2--%@",[NSThread currentThread]);
});
dispatch_barrier_sync(queueConcurrent, ^{
NSLog(@"barrier--%@",[NSThread currentThread]);
});
dispatch_async(queueConcurrent, ^{
NSLog(@"3--%@",[NSThread currentThread]);
});
dispatch_async(queueConcurrent, ^{
NSLog(@"4--%@",[NSThread currentThread]);
});
NSLog(@"end");
复制代码
我们看到dispatch_barrier_sync
确实阻塞了当前线程,end
是在dispatch_barrier_sync
之后输出的
dispatch_queue_t queueConcurrent = dispatch_queue_create("com.conc", DISPATCH_QUEUE_CONCURRENT);
dispatch_barrier_sync(queueConcurrent, ^{
NSLog(@"barrier--%@",[NSThread currentThread]);
dispatch_sync(queueConcurrent, ^{
NSLog(@"%@",[NSThread currentThread]);
});
});
复制代码
不知道这是不是文档中所说的Calling this function and targeting the current queue results in deadlock.
反正实现了deadlock
效果
另外文档提到了Block_copy
我从源码看到确实dispatch_barrier_async
调用了_dispatch_Block_copy
而dispatch_barrier_sync
是没有copy,这可能是因为dispatch_barrier_sync
阻塞线程后面代码不执行,而dispatch_barrier_async
没有阻塞线程,那么后面就可能对函数体修改????所以拷贝一份这里留下问好
GCD 延时执行方法:dispatch_after
2秒之后将任务添加到主队列,具体什么时候执行还要看CUP的调度
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0*NSEC_PER_SEC)), mainQueue, ^{
//2秒之后将任务添加到主队列,具体什么时候执行还要看CUP的调度
NSLog(@"执行了");
});
复制代码
GCD 一次性代码(只执行一次):dispatch_once
实现单例
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//单例
});
复制代码
更多内容可以看一下# iOS多线程之dispatch_once剖析
GCD 快速迭代方法:dispatch_apply
先来看一下文档dispatch_apply
可以看到dispatch_apply
和可重入且安全的并发队列可以实现高效的遍历操作,如果是一个串行队列那么就体现不出来他的高效之处了
dispatch_apply(10, queueGlobal, ^(size_t index) {
NSLog(@"%zd--%@",index,[NSThread currentThread]);
});
NSLog(@"end");
复制代码
dispatch_apply
会利用多个线程来遍历,不光是子线程,还可以调用主线程,另外他还会阻塞当前线程
Dispatch Group
dispatch_group
作为一个单元监控一组任务。你可以将一组任务放到一个组里通过dispatch_group
同步他们的行为。你可以将这些任务放在组里以后在同一个队列或者不同的队列异步执行,可以在不阻塞当前线程的情况下监听这些异步任务执行完毕,也可以阻塞当前线程等待这些任务完成。
dispatch_group_notify
dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queueGlobal, ^{
sleep(2);
NSLog(@"1--%@",[NSThread currentThread]);
});
dispatch_group_async(group, queueGlobal, ^{
NSLog(@"2--%@",[NSThread currentThread]);
});
dispatch_group_async(group, queueGlobal, ^{
sleep(1);
NSLog(@"3--%@",[NSThread currentThread]);
});
dispatch_group_notify(group, queueGlobal, ^{
NSLog(@"notify--%@",[NSThread currentThread]);
});
NSLog(@"end--%@",[NSThread currentThread]);
复制代码
dispatch_group_notify
并没有阻塞当前线程,他是在当前组的其他队列都执行完成之后再执行,我们可以将dispatch_group_notify
任务放在主队列执行,这就实现了回调主线程的功能
dispatch_queue_t queueSerial = dispatch_queue_create("com.serial", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queueMain = dispatch_get_main_queue();
dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queueGlobal, ^{
sleep(2);
NSLog(@"1--%@",[NSThread currentThread]);
});
dispatch_group_async(group, queueSerial, ^{
sleep(1);
NSLog(@"2--%@",[NSThread currentThread]);
});
dispatch_group_async(group, queueSerial, ^{
NSLog(@"3--%@",[NSThread currentThread]);
});
dispatch_group_notify(group, queueMain, ^{
NSLog(@"notify--%@",[NSThread currentThread]);
});
NSLog(@"end--%@",[NSThread currentThread]);
复制代码
dispatch_group_wait
阻塞当前线程,等待组内任务都执行完成才会继续执行
dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queueGlobal, ^{
sleep(2);
NSLog(@"1--%@",[NSThread currentThread]);
});
dispatch_group_async(group, queueGlobal, ^{
sleep(1);
NSLog(@"2--%@",[NSThread currentThread]);
});
dispatch_group_async(group, queueGlobal, ^{
NSLog(@"3--%@",[NSThread currentThread]);
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"end--%@",[NSThread currentThread]);
复制代码
直到前面三个任务都执行完成才会打印end,当然也可以将阻塞的超时时间设置小一些,即使前面任务没有完成,但是时间到了也会继续执行
dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queueGlobal, ^{
sleep(3);
NSLog(@"1--%@",[NSThread currentThread]);
});
dispatch_group_async(group, queueGlobal, ^{
sleep(1);
NSLog(@"2--%@",[NSThread currentThread]);
});
dispatch_group_async(group, queueGlobal, ^{
NSLog(@"3--%@",[NSThread currentThread]);
});
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2*NSEC_PER_SEC)));
NSLog(@"end--%@",[NSThread currentThread]);
复制代码
dispatch_group_enter、dispatch_group_leave
dispatch_group_async
内部也是通过dispatch_group_enter
和dispatch_group_leave
来实现的
dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_async(queueGlobal, ^{
sleep(3);
NSLog(@"1--%@",[NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queueGlobal, ^{
sleep(1);
NSLog(@"2--%@",[NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queueGlobal, ^{
NSLog(@"3--%@",[NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_notify(group, queueGlobal, ^{
NSLog(@"notify--%@",[NSThread currentThread]);
});
NSLog(@"end--%@",[NSThread currentThread]);
复制代码
实现了和dispatch_group_async
一样的效果
信号量semaphore
在开发中经常需要线程同步,那么信号量是一个很好的选择,dispatch_semaphore_signal
信号量+1,dispatch_semaphore_wait
信号量-1,如果信号量小于0那么阻塞当前线程,可以设置阻塞的超时时间
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(queueGlobal, ^{
sleep(3);//耗时操作
NSLog(@"1--%@",[NSThread currentThread]);
dispatch_semaphore_signal(semaphore);//信号量+1
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//信号量-1
dispatch_async(queueMain, ^{
NSLog(@"主线程");
});
NSLog(@"end--%@",[NSThread currentThread]);
复制代码
多线程访问同一个数据有可能造成数据不安全,例如
__block int i = 5;
while (i>0) {
dispatch_async(queueGlobal, ^{
i--;
NSLog(@"%d--%@",i,[NSThread currentThread]);
});
}
复制代码
由于多线程同时修改i
导致结果和预期出现了很大的出入,甚至NSLog
函数的打印都出问题了?,通过信号量可以解决这个问题
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block int i = 5;
while (i>0) {
dispatch_async(queueGlobal, ^{
i--;
NSLog(@"%d--%@",i,[NSThread currentThread]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
复制代码
这个时候我们看到结果按照预期输出了,因为信号量控制最多只有一个线程可以访问变量i
(不一定是同一个线程,但同一时间最多只能有一个线程访问)