本文内容:
- GCD相关概念
- 有关GCD的几道面试题
- 源码分析:队列和异步函数
GCD概念
GCD是Grand Central Dispatch的缩写。是苹果为提供多核并行运算而提出的解决方案。主要功能作用:将任务添加到队列,并且指定执行任务的函数
。而且开发人员不需要编写管理线程生命周期的代码。
任务
GCD中的任务用block
封装,并有以下特点:
- 任务
block
没有参数也没有返回值 - 不需要手动调用
block
,GCD内部帮我们调用
函数
GCD中的函数总体分为:同步函数dispatch_sync
和异步函数dispatch_async
- 同步函数
dispatch_sync
:- 等待当前语句执行完毕
- 不会开启线程
- 在当前线程执行任务
- 异步函数
dispatch_async
:- 不用等待当前语句执行完毕
- 会开启线程新线程执行任务
队列
队列是一种数据结构。具有先进先出的特性。GCD中大致分为两种队列类型,串行队列
和并发队列
。
根据调用不同的函数(同步or异步),会有以下四种情况:
--- | 同步函数 | 异步函数 |
---|---|---|
串行队列 | 1.不会开启线程 2.任务按顺序执行 3.会产生堵塞 | 1.开启新线程 2.任务按顺序执行 |
并发队列 | 1.不会开启线程 2.任务按顺序执行 | 1.开启新线程 2.任务异步执行,没有顺序,与CPU的调度有关 |
队列和线程的关系
面试的时候经常会被问到队列和线程之间的关系?
其实他们是没有太大的关系的,队列是一种数据结构,作为任务的容器
。线程是进程的基本执行单元,是任务的执行者
。CPU调度线程去执行容器中的任务。所以说队列和线程没有直接关系,只是在不同业务层级中担当不同的角色罢了。
GCD一些相关面试题
面试题1:
dispatch_queue_t queue = dispatch_queue_create("HelloGCD", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
答案:1、5、2、4、3
分析:队列是并行队列,两次调用异步函数(dispatch_async),都会开启新的线程执行任务,并且不会堵塞当前线程。
- 首先打印“1”,遇到异步函数不处理,然后打印“5”。
- 第一层异步函数内执行逻辑与外部类似,打印“2”和“4”。
- 最后执行第二次异步函数,打印“3”
面试题2
dispatch_queue_t queue = dispatch_queue_create("HelloGCD", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// sleep(2);
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
分析:并行队列添加相关任务,其中“3”是同步函数,会堵塞(堵塞的代码行在同步函数代码行结束的位置,也就是当前代码NSLog(@"3");
下一行的“});”
)当前线程。“0”在主线程中执行,其他的都是异步函数,所以“0”后面的异步函数肯定都会在“0”之后执行。因此本题答案是“3”在“0”之前,并且“7”、“8”、“9”在“0”之后。因此答案是AC。注意:“1”和“2”的位置不确定,这个取决于任务的时间复杂度,可以打开“1”中的sleep,打印查看一下结果,“1”会在“9”之后打印。
面试题3:
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("HelloGCD", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
答案:1、5、2、崩溃(EXC_BAD_INSTRUCTION)
分析:
- 在主线程队列中(串行队列),依次加入“1”、异步函数(dispatch_async)代码块、“5”
- 异步函数开启子线程,不阻塞主线程,所以先打印“1”和“5”。
- 子线程中,由于是串行队列,所以会把“2”、同步函数dispatch_sync、“4”这三个“任务”依次加入到queue中。
- 子线程开始串行执行任务,打印“2”
- 队列下一个任务是同步函数,会阻塞当前队列,然后把“3”加入到队列中,此时会产生死锁,此时队列情况:==dispatch_sync的block - “4” - “3”==
- 同步函数需要“3”执行完,自己才能执行结束。
- 由于“3”是在“4”后面加入到队列,所以“3”要等待“4”执行完成。
- “4”在同步函数后面加入到队列,所以得等待同步函数执行结束。
- 等待情况:dispatch_sync - “3” - “4” - dispatch_sync,是互相等待的状态,因此出现了死锁。
面试题4
__block int a = 0;
while (a < 5) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
a++;
});
}
NSLog(@"out a = %d", a);
问题1:最后的打印
A:0 B:<5 C:=5 D:>5
答案:CD
分析:
a初始化=0,进入while循环,循环条件是a<5,所以当a小于5的时候,都会在while循环中,所以可以排除A和B。循环中使用的是异步函数,异步函数会开辟线程,所以当其中一个线程的操作a++后满足跳出循环的条件了,就会退出循环,但是此时可能还会有其他线程还没有执行完,就会有a>=5的情况。因此答案是CD。
问题2:如何获取到循环中最后的a值
答案:在while循环外,使用相同的队列中,再次调用异步函数。
...
NSLog(@"out a = %d", a);
// add code
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
NSLog(@"out a = %d", a);
});
// end add
分析:
在相同队列中,以相同的方式(异步函数)再追加一个任务,任务内容是打印a,为了队列中其他任务执行完毕,此处增加一个sleep,因为任务都比较简单(NSLog),就算不加也不会有太大的问题。
这个问题在正常开发中不会使用到,而且会浪费很大的性能(会有很多无用的线程执行无用的任务)。目的只是考餐对GCD队列的了解程度。
问题3:如何进行性能优化
答案:
用信号量加锁的方式
dispatch_semaphore_t s = dispatch_semaphore_create(1);
__block int a = 0;
while (a < 5) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"in a = %d, %@", a, [NSThread currentThread]);
a++;
dispatch_semaphore_signal(s);
});
dispatch_semaphore_wait(s, DISPATCH_TIME_FOREVER);
}
NSLog(@"out a = %d", a);
分析:
此处信号量wait方法,如果放在异步函数(dispatch_async)调用之前,那么异步函数的就没有使用的意义了(编程顺序执行,可以把异步函数的代码删掉了)。放在异步函数之后,异步函数还是有意义的,不懂的可以自己打印看看打印结果。信号量加锁的方式很容易理解,但是两个函数放的位置,还有根据具体的业务需求来自行决定。
底层分析
源码libdispatch下载地址
队列创建源码分析
队列也是对象,通过一个示例证明一下:
- 代码中创建两个队列对象,一个是
串行队列
,另一个是并发队列
- 通过runtime的api方法
object_getClass
,查看他们的归属类:- 串行队列:
OS_dispatch_queue_serial
- 并发队列:
OS_dispatch_queue_concurrent
- 串行队列:
- 接下来通过查看源码找到类名创建队列的地方,以及isa的指向
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);
}
static dispatch_queue_t
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
dispatch_queue_t tq, bool legacy)
{
dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
......
const void *vtable;
if (dqai.dqai_concurrent) {
vtable = DISPATCH_VTABLE(queue_concurrent);
} else {
vtable = DISPATCH_VTABLE(queue_serial);
}
......
dispatch_lane_t dq = _dispatch_object_alloc(vtable,
sizeof(struct dispatch_lane_s)); // alloc
_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
(dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0)); // init
......
}
-
dispatch_queue_create
函数有两个参数:- 第一个
label
:字符标示 - 第二个
attr
:表示串行队列还是并发队列
- 第一个
- 紧接着调用
_dispatch_lane_create_with_target
函数,前两个参数就是dispatch_queue_create
的两个参数,第二个参数dqa
就是attr
- 对
dqa
封装成dispatch_queue_attr_info_t
类型,变量是dqai
,这里对传入的参数进行了判断,赋值给dqai.dqai_concurrent
,然后DISPATCH_VTABLE
宏获取不同队列的类名存入到vtable
变量中 - 后续调用
_dispatch_object_alloc
进行内存分配
void *
_dispatch_object_alloc(const void *vtable, size_t size)
{
#if OS_OBJECT_HAVE_OBJC1
//不关心的代码
.....
#else
return _os_object_alloc_realized(vtable, size);
#endif
}
inline _os_object_t
_os_object_alloc_realized(const void *cls, size_t size)
{
_os_object_t obj;
dispatch_assert(size >= sizeof(struct _os_object_s));
while (unlikely(!(obj = calloc(1u, size)))) {
_dispatch_temporary_resource_shortage();
}
obj->os_obj_isa = cls;//isa赋值
return obj;
}
- 调用
_dispatch_object_alloc
函数,间接调用_os_object_alloc_realized
函数 - 在
_os_object_alloc_realized
中看到了isa
赋值代码:obj->os_obj_isa = cls;
- 到此就我们就了解队列对象的整个初始化过程。
异步函数源码分析
主要的研究目标是任务block是如何被调用的。
dispatch_async(queue_c, ^{
NSLog(@"12334");
});
-
dispatch_async
有两个参数,第一个参数是队列,第二个是任务(block)
void
dispatch_async(dispatch_queue_t dq, dispatch_block_t work)
{
dispatch_continuation_t dc = _dispatch_continuation_alloc();
uintptr_t dc_flags = DC_FLAG_CONSUME;
dispatch_qos_t qos;
qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
_dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}
-
work
参数被传入到_dispatch_continuation_init
函数中的第三个参数,其余的地方没有用到
static inline dispatch_qos_t
_dispatch_continuation_init(dispatch_continuation_t dc,
dispatch_queue_class_t dqu, dispatch_block_t work,
dispatch_block_flags_t flags, uintptr_t dc_flags)
{
//封装work成ctxt
void *ctxt = _dispatch_Block_copy(work);
dc_flags |= DC_FLAG_BLOCK | DC_FLAG_ALLOCATED;
if (unlikely(_dispatch_block_has_private_data(work))) {
dc->dc_flags = dc_flags;
dc->dc_ctxt = ctxt;
// will initialize all fields but requires dc_flags & dc_ctxt to be set
return _dispatch_continuation_init_slow(dc, dqu, flags);
}
//封装work成func
dispatch_function_t func = _dispatch_Block_invoke(work);
if (dc_flags & DC_FLAG_CONSUME) {
func = _dispatch_call_block_and_release;
}
//ctxt和func作为参数传入
return _dispatch_continuation_init_f(dc, dqu, ctxt, func, flags, dc_flags);
}
-
work
被封装成ctxt
和func
,然后传入到_dispatch_continuation_init_f
函数中
static inline dispatch_qos_t
_dispatch_continuation_init_f(dispatch_continuation_t dc,
dispatch_queue_class_t dqu, void *ctxt, dispatch_function_t f,
dispatch_block_flags_t flags, uintptr_t dc_flags)
{
pthread_priority_t pp = 0;
dc->dc_flags = dc_flags | DC_FLAG_ALLOCATED;
dc->dc_func = f;//保存f
dc->dc_ctxt = ctxt;//保存ctxt
// in this context DISPATCH_BLOCK_HAS_PRIORITY means that the priority
// should not be propagated, only taken from the handler if it has one
if (!(flags & DISPATCH_BLOCK_HAS_PRIORITY)) {
pp = _dispatch_priority_propagate();
}
_dispatch_continuation_voucher_set(dc, flags);
return _dispatch_continuation_priority_set(dc, dqu, pp, flags);
}
- 到此我们看到了任务block的保存(dc->dc_ctxt = ctxt)和调用函数的保存(dc->dc_func = f),那么在什么时候调用呢?我们就需要查看调用堆栈了
- 在任务block内下断点,然后
bt
命令查看调用栈。
void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
@try {
return f(ctxt);//之前保存的相关调用方法和任务
}
@catch (...) {
objc_terminate();
}
}
void
_dispatch_call_block_and_release(void *block)
{
void (^b)(void) = block;
b();
Block_release(b);
}
- 可以看到最后调用的是
_dispatch_call_block_and_release
函数。这个函数就是上面源码中保存的f
,dc->dc_func = f;
- 此时就可以清楚为什么GCD相关的任务
block
不用我们手动调用了。