OC底层知识点之-多线程(二)GCD上篇
GCD简介
- GCD全称:Grand Central Dispatch
- GCD是纯C语言,提供了非常多的强大函数
- GCD是非常高效的多线程开发方式,它并不是Cocoa框架的一部分
GCD优势
- 1.GCD 是苹果公司为多核的并⾏运算提出的解决⽅案
- 2.GCD 会⾃动利⽤更多的CPU内核(⽐如双核、四核)
- 3.GCD 会⾃动管理线程的⽣命周期(创建线程、调度任务、销毁线程)
- 4.开发者只需要告诉 GCD 想要执⾏什么任务,不需要编写任何线程管理代码
【总结】:GCD就是将任务添加到队列,并且指定执行任务的函数。
GCD使用
在GCD使用中我们只需要做两件事:1.定义任务。2.将任务添加到队列中。所以GCD的核心就是dispatch队列和任务。
GCD队列
下面是GCD获取队列的集中方式:
- 1.主线程队列:提交的任务将会在主线程完成
- 可以通过dispatch_get_main_queue()来获得。
- 主队列就是主线程,它是一个串行队列,在iOS中只有主线程才能拥有权限向渲染服务提交图层信息,完成图形显示工作。所以和UI相关操作,必须在主线程执行。
- 2.全局并发队列(Clobal Queue):全局并发队列由整个进程共享,有高、中(默认)、低、后台四个优先级
- 3.自定义队列
- 并发队列:
- 全局队列是并发队列
- 通过dispatch_queue_create创建,第二个参数赋值为DISPATCH_QUEUE_CONCURRENT等
- 不用等待上个任务是否完成,直接启用新的线程执行新的任务。
- 串行队列:
- 通过dispatch_queue_create创建,第二个参数赋值为DISPATCH_QUEUE_SERIAL或者NULL。
- 串行队列在同一时间只能执行一个任务
- 并发队列:
GCD任务
GCD任务就是操作意思,就是你在block块中的代码通过什么方式执行。执行任务有两种方式:同步和异步,两者主要区别是:是否等待队列的任务执行结束,以及是否具备开辟线程的能力。
同步执行(sync)
- 1.同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
- 2.只能在当前线程中执行任务,不具备开启新线程的能力。
异步执行(async)
- 1.异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
- 2.可以在新的线程中执行任务,具备开启新线程的能力。
下面我们再将队列和任务搭配执行看看打印结果,准备代码
/**
同步并发
*/
- (void)concurrentSyncTest{
dispatch_queue_t queue = dispatch_queue_create("LJ", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10; i++) {
dispatch_sync(queue, ^{
NSLog(@"同步并发-%d-%@",i,[NSThread currentThread]);
});
}
}
/**
异步并发
*/
- (void)concurrentAsyncTest{
dispatch_queue_t queue = dispatch_queue_create("LJ", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10; i++) {
dispatch_async(queue, ^{
NSLog(@"异步并发-%d-%@",i,[NSThread currentThread]);
});
}
}
/**
串行异步
*/
- (void)serialAsyncTest{
dispatch_queue_t queue = dispatch_queue_create("LJ", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i<10; i++) {
dispatch_async(queue, ^{
NSLog(@"串行异步-%d-%@",i,[NSThread currentThread]);
});
}
}
/**
串行同步
*/
- (void)serialSyncTest{
dispatch_queue_t queue = dispatch_queue_create("LJ", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i<10; i++) {
dispatch_sync(queue, ^{
NSLog(@"串行同步-%d-%@",i,[NSThread currentThread]);
});
}
}
通过任务执行方式和不同队列组合,我们通过打印信息可以得出如下结论:
- 1.任务执行方式是
异步或者同步只能决定是否开辟新的线程
。同步(不开辟线程),异步(开辟新的线程) - 2.队列是
并行还是串行只能决定是否开辟多条线程
。串行(只开辟一条线程),并行(开辟多条线程,开辟多条线程的能力只有在异步执行中发挥作用
) - 3.
异步并行执行任务是乱序的
。
死锁
造成死锁的主要原因就是任务相互等待,看下面代码: 运行代码:发现报错了,报错原因就是死锁。下面我们分析下为什么会死锁: 这个方法有3步操作:
- 任务一:132行打印1任务,此部分在主线程。
- 任务二:137行打印3任务
- 任务三:134-136行通过同步任务向主线程插入打印2任务
我们知道主线程是同步任务
,任务一和任务二是先加入主线程,任务三会排在任务一,二后面
。但是任务三是通过同步任务加入
的。这就会出现下面的情况,任务三需要等待主线程执行完任务一,二后才会执行
。而同步任务的出现会让任务二等待任务三执行完成后才执行
,这就造成了在主线程中任务三等待任务二完成执行
,在同步任务里出现任务二等待任务三完成执行
,这就造成了相互等待。出现死锁崩溃 如下图所示更容易解释:
GCD原理初探
上面我们说了GCD的任务和队列,并通过代码打印来说明了任务和队列的关系,线面我们就来看看GCD的底层实现
确定GCD研究源码位置
我们想要研究GCD,却发现不知从哪入手,代码点击进去之后就走不下去了。那么我们怎么知道线程这部分的源码在哪呢?我们要确定源码,我们知道dispatch_queue_create方法可以创建线程
,那么我们打断点试试
这时就可以确定线程的源码在libdispatch.dylib中。我们在苹果的官方文档上下载libdispatch.dylib源码。
dispatch_get_main_queue()初探
我们先看dispatch_get_main_queue()主线程下图是对主线程的解释(捡主要的说一下):
- 569-570行:
主队列是用来在应用程序上下文中进行交互的主线程和主runloop。
- 579-580行:
主队列会被自动创建,而且会在main()函数之前创建
在main()函数前被调用,就是在dyld过程中进行的
dispatch_get_main_queue()再探
我们打印下主线程,来看看主线程是什么样的
dispatch_queue_t serial = dispatch_queue_create("Lj", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t conque = dispatch_queue_create("Lj", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0);
NSLog(@"%@-%@-%@-%@",serial,conque,mainQueue,globQueue);
运行打印
我们通过打印结果看到主线程变为了OS_dispatch_queue_main: com.apple.main-thread
,说明在底层系统进行命名为com.apple.main-thread
。我们在libdispatch源码里搜索com.apple.main-thread看看
共有8个文件,42个地方出现
下面那么我们应该怎么办?
libdispatch_init
多线程的调用最早是创建,我们在讲dyld的加载是提到过线程的加载:libdispatch_init
OC底层原理之-dyld加载流程传从门,搜索libdispatch_init
我们看到libdispatch_init方法很多,我们说主要方法,看下7759行代码,我们上面说的静态结构体_dispatch_main_q的do_targetq等于_dispatch_get_default_queue(true)
。后面就是对_dispatch_main_q进行一系列的操作(7762行:设置当前的主队列,7763行:绑定到相应的线程)
。下面我们查看下绑定过程:_dispatch_queue_set_bound_thread。
通过上图源码我们可以看到,绑定的底层实现是通过os_atomic_rmw_loop2o方法处理的,这部分实不在libdispatch源码中,后续有机会我们再研究。
总结
主线程下层是_dispatch_main_q的结构体
,它是在dyly加载中通过libdispatch_init方法进行创建
,它是一个相当于串行队列的队列
。
dispatch_get_global_queue
我们点击去看下:dispatch_get_global_queue需要传入两个参数:identifier和flags,注释对这两个参数进行了说明:
- identifier:服务质量(优先级)
- flags:预留使用
因为存在优先级,就说明整个项目中可以有多个dispatch_get_global_queue
,那么如何去设计它呢?我们可以想到通过集合去收集dispatch_get_global_queue
,下面我们通过com.apple.root.default-qos来查找一下dispatch_get_global_queue全局队列。
上图发现都是通过_DISPATCH_ROOT_QUEUE_ENTRY方法去创建的。这里面有各种各样不同优先级的全局并发队列。
我们再查看当前的结构体为_dispatch_root_queues,它也是一个静态结构体。
队列如何创建,DISPATCH_QUEUE_SERIAL和DISPATCH_QUEUE_CONCURRENT区别
上面我们简单的讲了下dispatch_get_main_queue()和dispatch_get_global_queue,知道他们底层是静态结构体。下面我们主要讲下队列的创建,以及串行和并行的实现原理
dispatch_queue_create
看下底层实现我们搜索_dispatch_lane_create_with_target看下其内部实现 方法很长,我们怎么研究?我们只需要关注返回值第2809行就可以了。它返回的就是我们的线程。下面我们看下_dispatch_trace_queue_create方法发现dispatch_queue_create是通过_dispatch_lane_create_with_target创建的,参数分别为label以及attr,后面的DISPATCH_TARGET_QUEUE_DEFAULT、true是默认值
我们看到
_dispatch_introspection_queue_create方法传入的是dq
,先创建dqic(653行创建,654行将dqic的dqic_queue._dq赋值为dq)
。659行又将dq的do_finalizer赋值为dqic
。之后就返回了upcast(dq)的_dqu。
上面并没有我们想要的东西。我们回到_dispatch_lane_create_with_target方法,再看返回值return _dispatch_trace_queue_create(dq)._dq;
这个方法返回的是_dq,上面我们知道_dispatch_introspection_queue_create返回的dq._dq中的dq是进行赋值,和传入的dq其实是同一个
。我们只需要研究_dispatch_lane_create_with_target传入的dq就可以了。
我们看到init方法里我们看到dqai.dqai_concurrent的属性,这个属性对线程的影响
下面我们看下dqai的创建 传入的dqa就是我们外界传入的值,我们看下_dispatch_queue_attr_to_info实现 我们看到dqai.dqai_concurrent跟idx相关,而idx跟dqa相关。而dqa就是我们在创建线程是传入的值(DISPATCH_QUEUE_CONCURRENT或者DISPATCH_QUEUE_SERIAL)我们看到
dqai.dqai_concurrent确定的值就是width
,1172行DOF_WIDTH()就是该队列支持的线程数,1就是串行(单线程),>1就是并行(多线程)
,也就是如果dqai.dqai_concurrent为true就是多线程,否则为单线程
。
这个截图是如果dqa==&_dispatch_queue_attr_concurrent就为true就是多线程。
我们再回到_dispatch_lane_create_with_target方法 如果是串行,vtable赋值传值为queue_concurrent,如果是并行vtable赋值传值为queue_serial,这样写是为了赋值,vtable也是个对象 通过上图我们可以知道:并行队列vtable为:OS_dispatch_queue_concurrent_class,而串行队列vtable为:OS_dispatch_queue_serial_class。而vtable对象应该为并行:OS_dispatch_queue_concurrent,串行:OS_dispatch_queue_serial 下面我们去打印并发和串行队列:我们发现串行和并行打印的结果和上面推测的一致。
我们再回到_dispatch_lane_create_with_target方法,继续看_dispatch_object_alloc方法。我们是onjc2所以会走_os_object_alloc_realized方法
,上面我们已经知道vtable在串行和并行赋值不同
,在_os_object_alloc_realized中vtable值就是cls
,1509行:创建是将isa指针指向了cls也就是指向了vtable
。
总结
队列创建底层是_dispatch_lane_create_with_target创建
,通过传入的值来确定是串行还是并行队列
,dispatch_queue_t也是个对象,也会通过alloc,init进行创建
。在alloc中将isa指针指向并发还是串行
,通过init来确定DOF_WIDTH()等属性
。
dispatch_async
上面讲了队列的创建,下面我们看下异步任务的实现- dq:就是穿过进来的队列
- work:就是传进来的任务
我们看下代码怎么操作的
- 890行:创建dc
- 896行:任务包装器,用来接收,保存block
2633-2638行:将work保存在dc的dc_ctxt中,其实这个判断是不走的,会走下面,
我们看下_dispatch_continuation_init_f,重点关注ctxt(将work进行copy)func(将work进行调用)
。注意:func在2642行执行了方法,也就是func执行完后会进行析构或者释放
。
此时我们看到上面参数
对应的就是ctxt和f
。方法将ctxt和f分别保存到dc的dc_ctxt和dc_func属性中
。
探究dispatch_async中work的执行
从上面知道work就是任务,我们探究下 此时的work就是打印123456,我们打断点,运行,然后bt一下我们看到start_wqthread,_pthread_wqthread是在libsystem_pthread源码中,而libdispatch源码中走的第一个方法就是_dispatch_worker_thread2。我们搜索一下
_dispatch_worker_thread2
红框就是下面执行的代码 在6581-6588行的循环中执行了_dispatch_continuation_pop_inline方法最后会调用_dispatch_continuation_init方法中的_dispatch_call_block_and_release最后
调用的是f(ctxt)方法
,我们在上面讲dispatch_async的_dispatch_continuation_init_f方法说了,最后会将调用任务方法放在f中,将任务放在ctxt中,此处得到验证
。
这就是block任务执行的整个流程。
拓展
相关面试题
【面试题 - 1】异步函数+并行队列 下面打印结果是什么?
- (void)textDemo2{
dispatch_queue_t queue = dispatch_queue_create("Lj", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
答案:1,5,2,4,3
解题:上面讲了,queue为并发队列,不会阻塞线程,所以1,5先执行。而并行队列里包含并行队列,所以他们任务互不影响。所以2,4先打印,最后为3。
代码修改
【修改1】:将并行队列 改成 串行队列,对结果没有任何影响,顺序仍然是 1 5 2 4 3
【修改2】:在任务5之前,休眠2s,即sleep(2),执行的顺序为:1 2 4 3 5,原因是因为I/O的打印,相比于休眠2s,复杂度更简单,所以异步block1 会先于任务5执行。当然如果主队列堵塞,会出现其他的执行顺序。
【修改3】:将打印 NSLog(@"3");的异步dispatch_async,改为同步dispatch_sync。执行顺序是:1,5,2,3,4,原因:将之前的异步改为同步够,会阻塞打印2的线程,导致只有打印3执行完后才能执行打印4。
【面试题 - 2】异步函数嵌套同步函数 + 串行队列(即同步队列)
- (void)textDemo2{
// 同步队列
dispatch_queue_t queue = dispatch_queue_create("Lj", NULL);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
答案:1,5,2崩溃 [图片上传失败...(image-338b41-1628941505662)]
原因:queue是串行队列,1,5,2正常打印不再解释,执行完2后(2当前线程为串行),打印3任务通过同步任务插入到串行队列,放在打印4的后面(在执行2串行任务里,4的打印在3的前面),但是同步任务有需要先执行3在执行4,就造成相互等待,造成死锁。
【修改】:将打印4去掉呢?
- 还是会死锁,因为任务3等待的是异步block执行完毕,而异步block等待任务3执行完成,还是会相互等待,造成死锁
【面试题 - 3】 异步函数 + 同步函数 + 并发队列
下面代码的执行顺序是什么?(答案是 AC)
- (void)interview04{
//并发队列
dispatch_queue_t queue = dispatch_queue_create("Lj", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // 耗时
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"0");
dispatch_async(queue, ^{
NSLog(@"7");
});
dispatch_async(queue, ^{
NSLog(@"8");
});
dispatch_async(queue, ^{
NSLog(@"9");
});
}
A: 1230789
B: 1237890
C: 3120798
D: 2137890
答案:AC
- 1.任务1 和 任务2由于是异步函数+并发队列,会开启线程,所以没有固定顺序
- 2.任务7、任务8、任务9同理,会开启线程,所以没有固定顺序
- 3.任务3是同步函数+并发队列,同步函数会阻塞主线程,但是也只会阻塞0,所以,可以确定的是 0一定在3之后,在789之前
【面试题 - 4】下面代码中,队列的类型有几种?
//串行队列 - Serial Dispatch Queue
dispatch_queue_t serialQueue = dispatch_queue_create("Lj", NULL);
//并发队列 - Concurrent Dispatch Queue
dispatch_queue_t concurrentQueue = dispatch_queue_create("Lj", DISPATCH_QUEUE_CONCURRENT);
//主队列 - Main Dispatch Queue
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//全局并发队列 - Global Dispatch Queue
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
答案:1.串行队列:serialQueue,mainQueue 2.并发队列:concurrentQueue,globalQueue