iOS 多线程原理 - 线程与队列底层
libdispatch-1271.120.2 下载
苹果官方资源opensource
多线程相关文献:
iOS 多线程原理 - 线程与队列底层
iOS 多线程原理 - GCD函数底层
iOS 线程底层 - 锁
本章节探究:
1.了解进程、线程
2.串行队列和并发队列
3.线程死锁的原因
4.同步函数 dispatch_sync 和 异步函数 dispatch_async
5.面试题
6.自定义线程池思想
一、概念相关
1.进程与线程
进程:
进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内 (通过“活动监视器”可以查看 Mac 系统中所开启的进程)。
线程:
线程是进程的基本执行单元,一个进程的所有任务都在线程中执行,进程要想执行任务,必须得有线程,进程至少要有一条线程,程序启动会默认开启一条线程,这条线程被称为主线程或 UI
线程。
进程与线程的关系:
- 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。
- 相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
- 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过
CPU
调度,在每个时间片中只有一个线程执行) - 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
- 资源拥有:同一进程内的线程共享本进程的资源如内存、
I/O
、cpu
等,但是进程之间的资源是独立的。 - 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 根本区别:进程是操作系统进行资源分配的基本单位,而线程是操作系统进行任务调度和执行的最小单位。
2.线程的声明周期
3.多线程
时间片的概念:CPU
在多个任务直接进行快速的切换,这个时间间隔就是时间片。
单核CPU
同一时间,CPU
只能处理 1 个线程上的任务。
多线程同时执行:
CPU
快速的在多个线程之间的切换,CPU
调度线程的时间足够快,就造成了多线程的“同时”执行的效果;如果线程数非常多,CPU
会在 N
个线程之间切换,消耗大量的 CPU
资源,每个线程被调度的次数会降低,线程的执行效率降低。
多线程的意义:
- 优点
- 能适当提高程序的执行效率
- 能适当提高资源的利用率(CPU,内存)
- 线程上的任务执行完成后,线程会自动销毁
- 缺点
- 开启线程需要占用一定的内存空间(默认情况下,主线程占1M,其它线程各占 512 KB)
- 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程越多,CPU 在调用线程上的开销就越大
- 程序设计更加复杂,比如线程间的通信、多线程的数据共享
开辟一条线程大概需要90微秒的时间。
// 获取设备能够支持线程的最大并发数量
NSLog(@"%ld", [NSProcessInfo processInfo].activeProcessorCount);
过多的开辟线程没有意义
4.线程池
GCD
内部维护了一个线程池去管理64条线程,在App
需要线程调度任务的时候实现复用;当前线程完成任务后就会被缓存到线程池里,下次再调用开辟线程的代码,GCD
会从线程池上找已经开辟且就绪状态
的线程。
所以开辟线程的代码,并不是真正意义上的开辟线程。尽管GCD
线程池里已有64条线程,但是最大并发数量还得是 [NSProcessInfo processInfo].activeProcessorCount;
。
5.GCD
GCD
全称是Grand Central Dispatch
,是苹果公司为多核的并行运算提出的解决方案,它是纯 C 语言并提供了非常多强大的函数;GCD
会自动利用更多的CPU
内核(比如双核、四核);GCD
会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。
程序员只需要告诉 GCD
想要执行什么任务,不需要编写任何线程管理代码。
6. 线程和Runloop的关系
- 1.
runloop
与线程是一一对应的,一个runloop
对应一个核心的线程,为什么说是核心的,是因为runloop
是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里 - 2.
runloop
是来管理线程的,当线程的runloop
被开启后,线程会在执行完任务后进入休眠
状态,有了任务就会被唤醒去执行任务 - 3.
runloop
在第一次获取时被创建,在线程结束时被销毁 - 4.对于
主线程
来说,runloop
在程序一启动就默认创建好了 - 5.对于
子线程
来说,runloop
是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop
被创建,不然定时器不会回调
二、串行队列 与 并发队列
队列和线程没有任何关系,队列是存储任务的,线程是从队列中取出任务去执行的。
队列分四种:串行队列
、并发队列
、全局并发队列
、主队列
队列的特性:先进先出 FIFO
- 获取主队列:
dispatch_get_main_queue
DISPATCH_INLINE DISPATCH_ALWAYS_INLINE DISPATCH_CONST DISPATCH_NOTHROW
dispatch_queue_main_t
dispatch_get_main_queue(void)
{
return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q); // _dispatch_main_q
}
DISPATCH_GLOBAL_OBJECT是一个宏定义好多地方有,没有办法定位到实际调用的哪个宏定义。但是通过lldb
打印堆栈bt
的话,又会多出好多别的函数调用不相关的东西。
打印主线程,它会有一个特定的名称:com.apple.main-thread
NSLog(@"%@",dispatch_get_main_queue());
// <OS_dispatch_queue_main: com.apple.main-thread>
源码里全局搜主线程名称,就能找到main_queue的初始化的地方
主队列是串行队列的一个标志性的东西:DQF_WIDTH(1)
在队列创建的时候看看源码就知道了。
- 创建队列:
dispatch_queue_create
// label: 队列名称 attr是串行队列还是并发列表
dispatch_queue_t
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
{
return _dispatch_lane_create_with_target(label, attr,
DISPATCH_TARGET_QUEUE_DEFAULT, true); // DISPATCH_TARGET_QUEUE_DEFAULT = NULL
}
只需要关心第二个参数attr
对于串行与并发的区别,它在_dispatch_lane_create_with_target
的形参名称是dqa
。
把dqa
封装成了dqai
,它是怎么封装的?
过多的不需要太关注拉。
再回来看看_dispatch_lane_create_with_target
初始化队列的步骤:
1.规范化参数 (qos, overcommit, tq)
2.初始化队列
初始化队列初始化的时候会判断串行并发标志位去限制width
是多少,串行指定是1,并发是14
来看看_dispatch_queue_init
的队列初始化,我们关注的队列是串行和并发的根本区别就是DQF_WIDTH(width)
,串行是DQF_WIDTH(1)
而dq->dq_serialnum
它其实是标志是这个队列是什么队列
串行队列与并发队列区别实质的
总结
:
DQF_WIDTH(1)
-串行队列
- 举例:单行道
DQF_WIDTH(>1)
-并发队列
- 举例:多车道
ps: 可以把队列看成是工厂流水线,保存着需要加工的部件,线程就是完成部件的工人。一条流水线有几个部件道就是串行队列与并发队列的区别。
三、线程死锁的原因
先来看看这个死锁现象
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{ // 这里产生了死锁
NSLog(@"这里不会来了");
});
NSLog(@"2");
}
造成线程死锁的原因:
NSLog(@"2");
的任务需要等待dispatch_sync
里的任务执行完才能执行,而dispatch_sync
里的任务是最后加入到主队列的,需要等待NSLog(@"2");
执行完才会执行。相互等待造成死锁
。
崩溃的信息也有展示出来:
打开libdispatch源码
搜索这个崩溃信息:__DISPATCH_WAIT_FOR_QUEUE__
可以清晰看到造成线程死锁会通过这个if
条件判断,解开这个条件判断相当于看清了造成死锁崩溃的本质了。(其实这段提示信息就已经解释了线程死锁的原因:dispatch_sync called on queue already owned by current thread
)
_dq_state_drain_locked_by
的源码声明:
_dispatch_lock_is_locked_by
的源码声明:
要产生死锁(这个函数返回true)必须是lock_value
与tid
是相等。
造成线程死锁的总结:
在和当前队列相关的线程 同步地 向串行队列添加任务,就会产生死锁。
死锁的必备条件:1.线程同步 2.串行队列
- 死锁案例:
- (void)viewDidLoad {
[super viewDidLoad];
// dispatch_sync不具备开辟线程的能力,所以一直在主线程工作。
dispatch_queue_t q = dispatch_queue_create("AnAn", DISPATCH_QUEUE_SERIAL);
dispatch_sync(q, ^{
NSLog(@"%@", [NSThread currentThread]); // main 主线程的环境是在q队列里
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{ // 死锁
NSLog(@"2"); // 主线程的环境是在主队列里,所以死锁了
});
NSLog(@"3");
});
NSLog(@"4");
}
// main 1 死锁
四、同步函数 与 异步函数
看同/异步函数的源码我们关注的点:
1.任务(block)的调用时机
2.关于线程相关的操作
- 同步函数
dispatch_sync
_dispatch_Block_invoke
其实就是任务(block)封装成Block_layout结构体
:
接下来需要关注_dispatch_sync_f
函数的第三个参数就是我们的任务(func),它是什么时候执行的。
_dispatch_sync_f
的源码声明:
_dispatch_sync_f_inline
的源码声明:
_dispatch_sync_f_inline
里面有很多个地方进行if
条件判断并使用了func
这是因为队列参数dq
有四种 主队列/串行队列/并发队列/全局并发队列
导致有很多种分支
由于libdispatch源码
是没办法编译的,所以我们可以在新建工程demo,并且在使用同步函数dispatch_sync
时打上符号断点
,哪里使用了func
就打上哪个符号,就可以拦截func
在不同情况下的去了哪个分支了。(也可以使用lldb
的调试命令bt
看看func
的去向)
以 global_queue + dispatch_sync
组合为例,进行调试
走到了_dispatch_sync_f_slow
分支,再来看看这个函数的源码
_dispatch_sync_f_slow
的源码声明:
demo上继续打上这俩函数符号断点,继续走
_dispatch_sync_function_invoke
的源码声明:
_dispatch_client_callout
的源码声明:
到这里dispatch_sync
的执行就结束了,别的组合有兴趣可以自己去试试。
回忆我们的关注点,在看dispatch_sync
源码的时候,并没有发现线程相关的操作,没有发现对任务的保存操作,任务在一直传递到底层代码后,立即被执行。
dispatch_sync
结论:
同步函数dispatch_sync :立即执行、阻塞当前线程、不具备开辟子线程的能力
- 异步函数
dispatch_async
-
_dispatch_continuation_init
保存任务,设置优先级
_dispatch_continuation_init
的源码声明:
_dispatch_continuation_init_f
的源码声明:
_dispatch_continuation_priority_set
是设置优先级,直接返回了qos
:
可以看到_dispatch_continuation_init
并没有对线程和任务执行的操作,仅仅只是保存了任务,在需要的时候拿出来执行。
_dispatch_continuation_async
_dispatch_continuation_async
的源码声明:
dx_push
是宏定义:
#define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)
找到dq_push
的声明:
根据不同的队列赋值给dq_push
不一样的函数
以并发队列为例:
_dispatch_lane_concurrent_push
的源码声明:
_dispatch_continuation_redirect_push
的源码声明:
这里会发现又走到了dx_push
,即递归了!综合前面队列创建时可知,队列也是一个对象,有父类、根类,所以会递归执行到根类的方法。
那do_targetq
是什么呢?得回到队列的创建dispatch_queue_create
去查看:
在dispatch_queue_create
的时候tq
就赋值出来是_dispatch_get_root_queue
了。
看看_dispatch_get_root_queue
的源码声明:
回到_dispatch_continuation_redirect_push
上面说它递归调用了dx_push
,此时它的类型却是dispatch_queue_global_t
了。(dx_push
是dq_push
的宏定义)
进去_dispatch_root_queue_push
:
进去_dispatch_root_queue_push_inline
:
进去_dispatch_root_queue_poke
:
进去_dispatch_root_queue_poke_slow
:
DISPATCH_NOINLINE
static void
_dispatch_root_queue_poke_slow(dispatch_queue_global_t dq, int n, int floor)
{
int remaining = n;
int r = ENOSYS;
_dispatch_root_queues_init();//重点
...
//do-while循环创建线程
do {
_dispatch_retain(dq); // released in _dispatch_worker_thread
while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
if (r != EAGAIN) {
(void)dispatch_assume_zero(r);
}
_dispatch_temporary_resource_shortage();
}
} while (--remaining);
...
}
走到了这里就进行了线程的操作啦。
分析一下:_dispatch_root_queues_init
DISPATCH_STATIC_GLOBAL(dispatch_once_t _dispatch_root_queues_pred);
DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_root_queues_init(void)
{
dispatch_once_f(&_dispatch_root_queues_pred, NULL,
_dispatch_root_queues_init_once);
}
发现是一个dispatch_once_f
单例(下面会介绍单例),其中传入的func
是_dispatch_root_queues_init_once
。
综上所述,异步函数
dispatch_async
的底层分析如下:【准备工作】:首先,将异步任务拷贝并封装,并设置回调函数
func
。
【block回调】:底层通过dx_push
递归,会重定向到根队列,然后通过pthread_creat
创建线程,最后通过dx_invoke
执行block
回调(注意dx_push
和dx_invoke
是成对的)。
总结
dispatch_async
中子线程创建
的调用流程:
1.dispatch_async
->_dispatch_continuation_async
->dx_push
->dq_push
->并发队列:_dispatch_lane_concurrent_push
->_dispatch_continuation_redirect_push
2.
_dispatch_continuation_redirect_push
->dx_push
(此时是global_queue
) ->_dispatch_root_queue_push
->_dispatch_root_queue_push_inline
->_dispatch_root_queue_poke
->_dispatch_root_queue_poke_slow
->线程池调度,创建线程pthread_create
总结同/异步函数特性:
同步函数dispatch_sync :
1. 阻塞当前线程进⾏等待,直到当前添加到队列的任务执⾏完成;
2. 只能在当前线程执⾏任务,不具备开启新线程的能⼒。
异步函数
dispatch_async:
1. 不会阻塞线程,不需要等待,任务可以继续执⾏;
2. 可以在新的线程执⾏任务,具备开启新线程的能⼒。(并发队列可以开启多条⼦线程,串⾏队列只能开启⼀条⼦线程)
五、面试题
ps: 注意要考虑任务复杂度
- (void)test1 {
dispatch_queue_t queue = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT); // 并发队列
NSLog(@"1");
dispatch_async(queue, ^{
// sleep(2);
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
//sleep(2);
NSLog(@"5");
}
// 1最前面 2在3前面 3在4前面 2和5没有顺序
- (void)test2 {
dispatch_queue_t queue = dispatch_queue_create("AnAn", DISPATCH_QUEUE_SERIAL); // 串行队列
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{ // 这里死锁
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
// 1最先 2和5没有顺序 死锁
- (void)test3 {
dispatch_queue_t queue = dispatch_queue_create("AnAn", DISPATCH_QUEUE_SERIAL); // 串行队列
dispatch_async(queue, ^{
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
});
NSLog(@"3");
});
// sleep(3); // 万一主线程这里复杂操作呢,把下面的任务延迟添加到queue队列
dispatch_async(queue, ^{
// sleep(3);
NSLog(@"4");
});
}
// 13 4和2没有顺序
- (void)test4 {
self.num = 0;
while (self.num < 100) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.num ++;
});
}
NSLog(@"self.num = %d",self.num);
}
// 比100大一点
- (void)test5 {
self.num = 0;
for (int i = 0; i < 100; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.num ++;
});
}
NSLog(@"self.num = %d",self.num);
}
// 0-100的其中一个数
-(void)test6 {
dispatch_queue_t queue = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
// 阻塞主线程
dispatch_sync(queue, ^{
// sleep(2); // 主线程
NSLog(@"3");
});
// sleep(2);
NSLog(@"0");
dispatch_async(queue, ^{
NSLog(@"7");
});
dispatch_async(queue, ^{
NSLog(@"8");
});
dispatch_async(queue, ^{
NSLog(@"9");
});
}
// 12789在子线程;30在主线程;3一定在0之前执行;789一定在30的后面执行
-(void)test7 {
dispatch_queue_t t = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_sync(t, ^{
NSLog(@"2");
dispatch_async(t, ^{
//sleep(2);
NSLog(@"3");
});
// sleep(2);
NSLog(@"4");
});
//sleep(2);
NSLog(@"5");
}
// 12一定先 5一定在4后面 3和5顺序不一定 3和4顺序不一定
-(void)test8 {
dispatch_queue_t t = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(t, ^{
// sleep(2);
NSLog(@"2");
dispatch_sync(t, ^{
// sleep(2);
NSLog(@"3");
});
// sleep(2);
NSLog(@"4");
});
// sleep(2);
NSLog(@"5");
}
// 125顺序不一定;3一定在2之后;4一定在3之后
-(void)test9 {
dispatch_queue_t t = dispatch_queue_create("lg", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_sync(t, ^{
// 在主线程
NSLog(@"2");
dispatch_async(t, ^{
// sleep(2); // 子线程
NSLog(@"3");
});
sleep(2);
NSLog(@"4");
});
// sleep(2);
NSLog(@"5");
}
// 124一定先 35顺序不一定
六、自定义线程池思想
通过分析YYKit
的线程池进行分析自己构造一个线程池的思想。准确来说YYKit
的线程池应该被叫做是队列池。
核心思想
:创建一个串行队列数组,数组里的每一个队列都进行异步操作任务,串行+异步=开辟一条线程的能力。每次需要完成任务时,从数组中轮询获取队列进行异步操作。
来看看YYKit
源码是如何实现一个线程池的。
准确来说不是创建线程,是从系统线程池的64条线程中拿到对应个数的线程。