前言
函数与队列的一个具体结合的应用示例,就是苹果推出的GCD(Grand Central Dispatch)
,GCD
是苹果公司为多核的并行运算提出的解决方案,它会自动利用更多的CPU内核(比如双核、四核),会自动管理线程的生命周期
(创建线程、调度任务、销毁线程), 程序员只需要告诉 GCD想要执行什么任务,不需要编写任何线程管理代码。
一、函数与队列的概念
-
函数可以这么理解,就是调度任务,执行任务,说白可就是你想要做的事情。
在GCD中任务就是👇- 任务使用
block封装
- 任务的 block
没有参数
也没有返回值
- 任务分为两种类型:
同步
&异步
- 任务使用
- 同步任务
- 必须等待当前语句执行完毕,才会执行下一条语句
- 不会开启线程
- 在当前线程执行 block 的任务
- 异步任务
- 不用等待当前语句执行完毕,就可以执行下一条语句
- 会开启新线程执行 block 的任务
- 异步是多线程的代名词
- 队列是一种数据结构,遵循FIFO的原则,先进来先调度,注意是调度,而不是执行。
函数与队列的关系示例👇
- (void)syncTest{
// 把任务添加到队列 --> 函数
// 任务 --> dispatch_block_t ref,是个c对象
dispatch_block_t block = ^{
NSLog(@"hello GCD");
};
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("com.lg.cn", NULL);
// 函数
dispatch_async(queue, block);
}
二、函数与队列的搭配
大致有以下4种情况:
-
同步函数串行队列
- 不会开启线程,在当前线程执行任务
- 任务串行执行,任务一个接一个
- 会产生堵塞
-
同步函数并发队列
- 不会开启新的线程,在当前线程中执行任务
- 任务一个接一个
-
异步函数串行队列
- 开启一条新线程
- 任务一个接一个
-
异步函数并发队列
- 开启子线程,在当前线程中执行任务
- 任务异步执行,没有顺序,与CPU调度有关
三、相关的面试题剖析
3.1 耗时问题案例
- (void)wbinterDemo2 {
// 耗能问题 - 同步和异步
// 用 - 并发 多线程问题
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("com.lgcooci.cn", DISPATCH_QUEUE_SERIAL);
// dispatch_async(queue, ^{
// });
////
dispatch_sync(queue, ^{
});
NSLog(@"%f",CFAbsoluteTimeGetCurrent()-time);
}
同步和异步都会耗时
,那为什么还要使用,因为它们可以解决多线程同步和并发的一系列问题。
3.2 并发队列案例
- (void)textDemo{
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
// 耗时
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
分析:
- queue是一个并发队列
-
NSLog(@"1")
块dispatch_async(queue, ^{})
和NSLog(@"5")
是依次添加到这个queue中,这3个任务遵循FIFO原则 - 再看
块dispatch_async(queue, ^{})
,这里执行任务有3个,NSLog(@"2")
块dispatch_async(queue, ^{ NSLog(@"3"); })
和NSLog(@"4")
,所以该块任务比较耗时 -
本身dispatch_async耗时,所以外层块任务执行顺序是 1> 5 > 子块,同理子块的执行顺序是 2 > 4 > 3,综合结论:1>5>2>4>3
运行👇
3.3 死锁案例
稍微改下3.2的案例,将队列换成串行,同时将子块的异步换成同步
- (void)textDemo2{
// 同步队列
dispatch_queue_t queue = dispatch_queue_create("cooci", NULL);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
运行👇
直接报错,报错前运行结果是1 > 5 > 2,为什么会这样?
分析:
- 出队列的情况如下👇
- 异步块
块dispatch_async(queue, ^{})
中有个同步块dispatch_async(queue, ^{ NSLog(@"3"); })
,而同步块中的3是在异步块之后加入
串行队列的,按照FIFO原则,异步块出队列之后,同步块才能出队列
,但是同步块里必须等待当前语句NSLog(@"3");
执行完毕,才会执行下一条语句NSLog(@"4");
,然后异步块执行完毕,这样就形成了一个相互等待,异步块等待同步块dispatch_async(queue, ^{ NSLog(@"3"); })
,同步块又继续等待异步块(队列的FIFO原则),如下图👇
- 这个相互等待的现象被称作
死锁
,死锁之前呢1,5和2已经出了队列,所以打印出来的就是1 > 5 > 2。 - 死锁问题,在堆栈中反应就是
_dispatch_sync_f_slow
。
3.4 同步阻塞案例
- (void)wbinterDemo{
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
// 1 2 3
// 0 (7 8 9)
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
}
这个案例比上面的死锁
案例就简单很多了,根据同步块必须等待当前语句执行完毕,才会执行下一条语句
这个原则,那么NSLog(@"0");
肯定在NSLog(@"3");
之后执行,同时NSLog(@"7");
NSLog(@"8");
和NSLog(@"9");
都是异步块,执行顺序不固定,但是异步块都会有耗时,所以它们必须在NSLog(@"0");
之后执行,所以3 > 0 > 7 8 9(无固定顺序)
,这样满足条件的选项只有AC
。
运行看看👇
四、队列的种类
队列的种类就两种:串行队列 & 并发队列
-
串行队列(Serial Dispatch Queue):
一次只调度一个任务,队列中的任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务),并且只会开启一条线程
/**
串行异步队列
*/
- (void)serialAsyncTest{
//1:创建串行队列
dispatch_queue_t queue = dispatch_queue_create("Cooci", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i<20; i++) {
dispatch_async(queue, ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
NSLog(@"hello queue");
}
/**
串行同步队列 : FIFO: 先进先出
*/
- (void)serialSyncTest{
//1:创建串行队列
dispatch_queue_t queue = dispatch_queue_create("Cooci", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i<20; i++) {
dispatch_sync(queue, ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
}
运行👇
串行队列异步任务
串行队列同步任务
-
并发队列(Concurrent Dispatch Queue):
- 多个任务同时执行,会开启多条线程执行
- 但是有个特例-->
并发队列下执行同步任务
,不会开启新的线程
/**
异步并发
*/
- (void)concurrentAsyncTest{
dispatch_queue_t queue = dispatch_queue_create("Cooci", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<20; i++) {
dispatch_async(queue, ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
NSLog(@"hello queue");
}
/**
同步并发
*/
- (void)concurrentSyncTest{
dispatch_queue_t queue = dispatch_queue_create("Cooci", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<20; i++) {
dispatch_sync(queue, ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
NSLog(@"hello queue");
}
运行👇
并发队列异步任务
上图可见,在并发队列执行异步任务
时,会开启多条新的线程,且执行顺序我们无法控制,至于是哪条线程执行任务由队列决定,哪个任务先完成由CPU决定
。例如结果中number = 6或number = 5等子线程执行了多次任务,那是因为这些子线程执行完任务就会被线程池回收,队列再从线程池中取线程执行任务,这时就会线程重复利用,详细流程可参考多线程里的线程池调度
章节。
并发队列下执行同步任务
不会创建新线程,所有任务依次在主线程上
执行。
两种很常用的队列
主队列 dispatch_get_main_queue
全局队列 dispatch_get_global_queue
4.1 dispatch_get_main_queue底层
cmd+shitf+字母o
搜索dispatch_get_main_queue
查看声明的注释,可知2个重要点:
- 主队列依赖当前的主线程和其RunLoop
- 主队列的指向在main()方法调用之前自动创建的
之前我们分析类的加载的流程可知,在main()调用之前,要么是dyld
的调起动态库去调用main方法,要么是objc_init
中的readImage
回调中去触发main方法。其中dyld
会加载libdispatch.dylib
这个库。如何验证呢?--> 符号断点
通过符号断点
打dispatch_get_main_queue
,打dispatch_sync
可见,查到GCD底层是libdispatch.dylib
这个动态库。
然后去官网下载源码,搜索_dispatch_main_q
👇
_dispatch_main_q 的类型是dispatch_queue_static_s
,一个静态结构体,我们自定义队列的时候,需要指定队列的name,上图可知,主队列有个dq_label
是com.apple.main-thread
,这个是不是主队列的name呢?我们可以lldb打印看看👇
果然,com.apple.main-thread
这就是主队列的name,dq_label
是name字段的key值。
那么问题来了,
主队列的类型是串行还是并发?
我们全局搜索一下_dispatch_main_q
,看看在哪里初始化的?
我们找到了libdispatch_init(void)
,
框里的两句代码,将
主队列
设置为当前的默认队列,再看_dispatch_queue_set_bound_thread
源码👇
DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_queue_set_bound_thread(dispatch_queue_class_t dqu)
{
// Tag thread-bound queues with the owning thread
dispatch_assert(_dispatch_queue_is_thread_bound(dqu));
uint64_t old_state, new_state;
os_atomic_rmw_loop2o(dqu._dq, dq_state, old_state, new_state, relaxed, {
new_state = old_state;
new_state &= ~DISPATCH_QUEUE_DRAIN_OWNER_MASK;
new_state |= _dispatch_lock_value_for_self();
});
}
大致是即将主队列与主线程进行绑定。
目前为止,没有发现主队列是串行或并发的相关代码。暂且我们先看看全局队列的源码,找找有没有相关的联系。
4.2 dispatch_get_global_queue底层
dispatch_queue_global_t
dispatch_get_global_queue(long priority, unsigned long flags)
{
dispatch_assert(countof(_dispatch_root_queues) ==
DISPATCH_ROOT_QUEUE_COUNT);
if (flags & ~(unsigned long)DISPATCH_QUEUE_OVERCOMMIT) {
return DISPATCH_BAD_INPUT;
}
dispatch_qos_t qos = _dispatch_qos_from_queue_priority(priority);
#if !HAVE_PTHREAD_WORKQUEUE_QOS
if (qos == QOS_CLASS_MAINTENANCE) {
qos = DISPATCH_QOS_BACKGROUND;
} else if (qos == QOS_CLASS_USER_INTERACTIVE) {
qos = DISPATCH_QOS_USER_INITIATED;
}
#endif
if (qos == DISPATCH_QOS_UNSPECIFIED) {
return DISPATCH_BAD_INPUT;
}
return _dispatch_get_root_queue(qos, flags & DISPATCH_QUEUE_OVERCOMMIT);
}
返回值是dispatch_queue_global_t
类型,再搜索dispatch_queue_global_t
👇
是结构体dispatch_queue_global_s
的指针。
接着我们再来反向操作,根据线程name名称搜索👇
搜索
com.apple.root.default-qos
👇再次验证是结构体
dispatch_queue_global_s
,属于一个集合_dispatch_root_queues[]
的一个元素。
再对比看看 _dispatch_main_q
和dispatch_queue_global_s
DISPATCH_QUEUE_WIDTH_POOL
宏定义👇此时,我们知道了
dq_atomic_flags
(可以理解为队列宽度),主队列是1,而全局队列是0x1000 - 1 -->转换成10进制就是4096-1 = 4095。大致可以看出,主队列应该是串行队列,而全局队列是并发的
。
五、队列queue的底层
如何验证主队列是串行,全局队列是并发
?这还要从队列的底层源码看起,我们先看看队列的创建。
5.1 队列queue的创建
dispatch_queue_create
源码👇
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_lane_create_with_target
根据返回值
_dispatch_trace_queue_create(dq)._dq;
,再来看看_dispatch_trace_queue_create
👇再回头来看
_dispatch_trace_queue_create(dq)._dq
,其实_dispatch_trace_queue_create(dq)
就是将dq里的某些字段
完成了一系列的初始化
,最后取字段_dq
返回,重点就来到 -->dq的创建过程,然后它的字段_dq在哪里赋值
。
dq创建相关的代码👇
继续,看看初始化
_dispatch_queue_init
👇
很明显,和
_dispatch_trace_queue_create(dq)
如出一辙,dqu
的初始化依赖dqf
width
和initial_state_bits
。再来看这3个入参的来源。
- 先看
dqf
来到dqai
的创建👇
最终定位到dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
-
width
和initial_state_bits
_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
(dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));
根据上述源码,其实width
和initial_state_bits
的值的来源-->也是dqai.dqai_concurrent
和 dqai.dqai_inactive
👇
size_t idx = (size_t)(dqa - _dispatch_queue_attrs);
dqai.dqai_inactive = (idx % DISPATCH_QUEUE_ATTR_INACTIVE_COUNT);
idx /= DISPATCH_QUEUE_ATTR_INACTIVE_COUNT;
dqai.dqai_concurrent = !(idx % DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT);
idx /= DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT;
其中宏定义👇
#define DISPATCH_QUEUE_ATTR_INACTIVE_COUNT 2
#define DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT 2
到此,我们研究的重点最终来到了dqai
。
5.2 串行队列和并发队列的区别
还是按照溯源
的原则,dqai
的创建👇
// dqai 创建 -
dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
dqa是方法入参👇
那么就是attr
👇
// 并发
dispatch_queue_t conque = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
// 串行
dispatch_queue_t serial = dispatch_queue_create("cooci", DISPATCH_QUEUE_SERIAL);
不就是入参DISPATCH_QUEUE_CONCURRENT
or DISPATCH_QUEUE_SERIAL
么。
再回头看看主队列
和全局队列
,他们分别是什么类型的👇
主队列
上面我们分析过,主队列dispatch_get_main_queue
对应的底层结构体是_dispatch_main_q
👇
然后看队列的初始化中👇,
dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1
这个三元表达式对应的参数是uint16_t width
👇width
经过处理,赋值给了dq->dq_state
。回头看主队列的dq_state
👇
都有一个DISPATCH_QUEUE_STATE_INIT_VALUE
,并且主队列DISPATCH_QUEUE_STATE_INIT_VALUE(1)
表示width是1
,根据三元表达式dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1
,串行队列才是1,所以主队列是串行队列
。
全局队列
上面分析,全局队列dispatch_get_global_queue
对应的底层结构是dispatch_queue_global_s
👇
其中dq_state = DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE
定义👇
#define DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE \
(DISPATCH_QUEUE_WIDTH_FULL_BIT | DISPATCH_QUEUE_IN_BARRIER)
#define DISPATCH_QUEUE_WIDTH_FULL_BIT 0x0020000000000000ull
#define DISPATCH_QUEUE_IN_BARRIER 0x0040000000000000ull
同理,根据dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1
,那么并发队列
的话width
是DISPATCH_QUEUE_WIDTH_MAX
,其定义👇
#define DISPATCH_QUEUE_WIDTH_FULL 0x1000ull
#define DISPATCH_QUEUE_WIDTH_MAX (DISPATCH_QUEUE_WIDTH_FULL - 2)
至此,我们知道并发队列
的width
是DISPATCH_QUEUE_WIDTH_FULL - 2
,必定大于1
,
而全局队列
的.dq_state = DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE
肯定不等于DISPATCH_QUEUE_STATE_INIT_VALUE(1)
,那么全局队列是并发队列
。
综上所述,我们得出结论👇
主队列是串行队列,全局队列是并发队列。
网友评论