本文內(nèi)容:
- GCD相關(guān)概念
- 有關(guān)GCD的幾道面試題
- 源碼分析:隊(duì)列和異步函數(shù)
GCD概念
GCD是Grand Central Dispatch的縮寫(xiě)赔硫。是蘋(píng)果為提供多核并行運(yùn)算而提出的解決方案贡这。主要功能作用:將任務(wù)添加到隊(duì)列,并且指定執(zhí)行任務(wù)的函數(shù)
眼滤。而且開(kāi)發(fā)人員不需要編寫(xiě)管理線(xiàn)程生命周期的代碼赏僧。
任務(wù)
GCD中的任務(wù)用block
封裝大猛,并有以下特點(diǎn):
- 任務(wù)
block
沒(méi)有參數(shù)也沒(méi)有返回值 - 不需要手動(dòng)調(diào)用
block
,GCD內(nèi)部幫我們調(diào)用
函數(shù)
GCD中的函數(shù)總體分為:同步函數(shù)dispatch_sync
和異步函數(shù)dispatch_async
- 同步函數(shù)
dispatch_sync
:- 等待當(dāng)前語(yǔ)句執(zhí)行完畢
- 不會(huì)開(kāi)啟線(xiàn)程
- 在當(dāng)前線(xiàn)程執(zhí)行任務(wù)
- 異步函數(shù)
dispatch_async
:- 不用等待當(dāng)前語(yǔ)句執(zhí)行完畢
- 會(huì)開(kāi)啟線(xiàn)程新線(xiàn)程執(zhí)行任務(wù)
隊(duì)列
隊(duì)列是一種數(shù)據(jù)結(jié)構(gòu)淀零。具有先進(jìn)先出的特性挽绩。GCD中大致分為兩種隊(duì)列類(lèi)型,串行隊(duì)列
和并發(fā)隊(duì)列
驾中。
根據(jù)調(diào)用不同的函數(shù)(同步or異步)唉堪,會(huì)有以下四種情況:
--- | 同步函數(shù) | 異步函數(shù) |
---|---|---|
串行隊(duì)列 | 1.不會(huì)開(kāi)啟線(xiàn)程 2.任務(wù)按順序執(zhí)行 3.會(huì)產(chǎn)生堵塞 | 1.開(kāi)啟新線(xiàn)程 2.任務(wù)按順序執(zhí)行 |
并發(fā)隊(duì)列 | 1.不會(huì)開(kāi)啟線(xiàn)程 2.任務(wù)按順序執(zhí)行 | 1.開(kāi)啟新線(xiàn)程 2.任務(wù)異步執(zhí)行,沒(méi)有順序链方,與CPU的調(diào)度有關(guān) |
隊(duì)列和線(xiàn)程的關(guān)系
面試的時(shí)候經(jīng)常會(huì)被問(wèn)到隊(duì)列和線(xiàn)程之間的關(guān)系祟蚀?
其實(shí)他們是沒(méi)有太大的關(guān)系的前酿,隊(duì)列是一種數(shù)據(jù)結(jié)構(gòu)薪者,作為任務(wù)的容器
剿涮。線(xiàn)程是進(jìn)程的基本執(zhí)行單元取试,是任務(wù)的執(zhí)行者
瞬浓。CPU調(diào)度線(xiàn)程去執(zhí)行容器中的任務(wù)猿棉。所以說(shuō)隊(duì)列和線(xiàn)程沒(méi)有直接關(guān)系萨赁,只是在不同業(yè)務(wù)層級(jí)中擔(dān)當(dāng)不同的角色罷了杖爽。
GCD一些相關(guān)面試題
面試題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
分析:隊(duì)列是并行隊(duì)列赖阻,兩次調(diào)用異步函數(shù)(dispatch_async)火欧,都會(huì)開(kāi)啟新的線(xiàn)程執(zhí)行任務(wù)茎截,并且不會(huì)堵塞當(dāng)前線(xiàn)程企锌。
- 首先打印“1”,遇到異步函數(shù)不處理撕攒,然后打印“5”抖坪。
- 第一層異步函數(shù)內(nèi)執(zhí)行邏輯與外部類(lèi)似擦俐,打印“2”和“4”蚯瞧。
- 最后執(zhí)行第二次異步函數(shù),打印“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
分析:并行隊(duì)列添加相關(guān)任務(wù)备徐,其中“3”是同步函數(shù)坦喘,會(huì)堵塞(堵塞的代碼行在同步函數(shù)代碼行結(jié)束的位置瓣铣,也就是當(dāng)前代碼NSLog(@"3");
下一行的“});”
)當(dāng)前線(xiàn)程棠笑”途龋“0”在主線(xiàn)程中執(zhí)行,其他的都是異步函數(shù)斩例,所以“0”后面的異步函數(shù)肯定都會(huì)在“0”之后執(zhí)行念赶。因此本題答案是“3”在“0”之前叉谜,并且“7”踩萎、“8”香府、“9”在“0”之后裆泳。因此答案是AC柠硕。注意:“1”和“2”的位置不確定蝗柔,這個(gè)取決于任務(wù)的時(shí)間復(fù)雜度癣丧,可以打開(kāi)“1”中的sleep胁编,打印查看一下結(jié)果嬉橙,“1”會(huì)在“9”之后打印寥假。
面試題3:
// 串行隊(duì)列
dispatch_queue_t queue = dispatch_queue_create("HelloGCD", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
// 異步函數(shù)
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
答案:1糕韧、5喻圃、2斧拍、崩潰(EXC_BAD_INSTRUCTION)
分析:
- 在主線(xiàn)程隊(duì)列中(串行隊(duì)列)肆汹,依次加入“1”县踢、異步函數(shù)(dispatch_async)代碼塊硼啤、“5”
- 異步函數(shù)開(kāi)啟子線(xiàn)程谴返,不阻塞主線(xiàn)程嗓袱,所以先打印“1”和“5”渠抹。
- 子線(xiàn)程中梧却,由于是串行隊(duì)列放航,所以會(huì)把“2”广鳍、同步函數(shù)dispatch_sync赊时、“4”這三個(gè)“任務(wù)”依次加入到queue中蛋叼。
- 子線(xiàn)程開(kāi)始串行執(zhí)行任務(wù)狐胎,打印“2”
- 隊(duì)列下一個(gè)任務(wù)是同步函數(shù)握巢,會(huì)阻塞當(dāng)前隊(duì)列松却,然后把“3”加入到隊(duì)列中晓锻,此時(shí)會(huì)產(chǎn)生死鎖独撇,此時(shí)隊(duì)列情況:==dispatch_sync的block - “4” - “3”==
- 同步函數(shù)需要“3”執(zhí)行完躁锁,自己才能執(zhí)行結(jié)束战转。
- 由于“3”是在“4”后面加入到隊(duì)列啄踊,所以“3”要等待“4”執(zhí)行完成刁标。
- “4”在同步函數(shù)后面加入到隊(duì)列,所以得等待同步函數(shù)執(zhí)行結(jié)束吏砂。
- 等待情況:dispatch_sync - “3” - “4” - dispatch_sync狐血,是互相等待的狀態(tài)匈织,因此出現(xiàn)了死鎖。
面試題4
__block int a = 0;
while (a < 5) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
a++;
});
}
NSLog(@"out a = %d", a);
問(wèn)題1:最后的打印
A:0 B:<5 C:=5 D:>5
答案:CD
分析:
a初始化=0缀匕,進(jìn)入while循環(huán)纳决,循環(huán)條件是a<5,所以當(dāng)a小于5的時(shí)候乡小,都會(huì)在while循環(huán)中阔加,所以可以排除A和B。循環(huán)中使用的是異步函數(shù)满钟,異步函數(shù)會(huì)開(kāi)辟線(xiàn)程胜榔,所以當(dāng)其中一個(gè)線(xiàn)程的操作a++后滿(mǎn)足跳出循環(huán)的條件了,就會(huì)退出循環(huán)湃番,但是此時(shí)可能還會(huì)有其他線(xiàn)程還沒(méi)有執(zhí)行完夭织,就會(huì)有a>=5的情況尊惰。因此答案是CD逾条。
問(wèn)題2:如何獲取到循環(huán)中最后的a值
答案:在while循環(huán)外,使用相同的隊(duì)列中,再次調(diào)用異步函數(shù)安券。
...
NSLog(@"out a = %d", a);
// add code
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
NSLog(@"out a = %d", a);
});
// end add
分析:
在相同隊(duì)列中址貌,以相同的方式(異步函數(shù))再追加一個(gè)任務(wù),任務(wù)內(nèi)容是打印a赠制,為了隊(duì)列中其他任務(wù)執(zhí)行完畢政恍,此處增加一個(gè)sleep宗弯,因?yàn)槿蝿?wù)都比較簡(jiǎn)單(NSLog)邓厕,就算不加也不會(huì)有太大的問(wèn)題昧互。
這個(gè)問(wèn)題在正常開(kāi)發(fā)中不會(huì)使用到贿讹,而且會(huì)浪費(fèi)很大的性能(會(huì)有很多無(wú)用的線(xiàn)程執(zhí)行無(wú)用的任務(wù))面殖。目的只是考餐對(duì)GCD隊(duì)列的了解程度辽幌。
問(wèn)題3:如何進(jìn)行性能優(yōu)化
答案:
用信號(hào)量加鎖的方式
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);
分析:
此處信號(hào)量wait方法哭当,如果放在異步函數(shù)(dispatch_async)調(diào)用之前脖岛,那么異步函數(shù)的就沒(méi)有使用的意義了(編程順序執(zhí)行绍在,可以把異步函數(shù)的代碼刪掉了)溜宽。放在異步函數(shù)之后嫉嘀,異步函數(shù)還是有意義的,不懂的可以自己打印看看打印結(jié)果推掸。信號(hào)量加鎖的方式很容易理解,但是兩個(gè)函數(shù)放的位置廊遍,還有根據(jù)具體的業(yè)務(wù)需求來(lái)自行決定见咒。
底層分析
源碼libdispatch下載地址
隊(duì)列創(chuàng)建源碼分析
隊(duì)列也是對(duì)象,通過(guò)一個(gè)示例證明一下:
- 代碼中創(chuàng)建兩個(gè)隊(duì)列對(duì)象,一個(gè)是
串行隊(duì)列
腾仅,另一個(gè)是并發(fā)隊(duì)列
- 通過(guò)runtime的api方法
object_getClass
推励,查看他們的歸屬類(lèi):- 串行隊(duì)列:
OS_dispatch_queue_serial
- 并發(fā)隊(duì)列:
OS_dispatch_queue_concurrent
- 串行隊(duì)列:
- 接下來(lái)通過(guò)查看源碼找到類(lèi)名創(chuàng)建隊(duì)列的地方,以及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
函數(shù)有兩個(gè)參數(shù):- 第一個(gè)
label
:字符標(biāo)示 - 第二個(gè)
attr
:表示串行隊(duì)列還是并發(fā)隊(duì)列
- 第一個(gè)
- 緊接著調(diào)用
_dispatch_lane_create_with_target
函數(shù)醉拓,前兩個(gè)參數(shù)就是dispatch_queue_create
的兩個(gè)參數(shù),第二個(gè)參數(shù)dqa
就是attr
- 對(duì)
dqa
封裝成dispatch_queue_attr_info_t
類(lèi)型,變量是dqai
伦乔,這里對(duì)傳入的參數(shù)進(jìn)行了判斷厉亏,賦值給dqai.dqai_concurrent
,然后DISPATCH_VTABLE
宏獲取不同隊(duì)列的類(lèi)名存入到vtable
變量中 - 后續(xù)調(diào)用
_dispatch_object_alloc
進(jìn)行內(nèi)存分配
void *
_dispatch_object_alloc(const void *vtable, size_t size)
{
#if OS_OBJECT_HAVE_OBJC1
//不關(guān)心的代碼
.....
#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;
}
- 調(diào)用
_dispatch_object_alloc
函數(shù)烈和,間接調(diào)用_os_object_alloc_realized
函數(shù) - 在
_os_object_alloc_realized
中看到了isa
賦值代碼:obj->os_obj_isa = cls;
- 到此就我們就了解隊(duì)列對(duì)象的整個(gè)初始化過(guò)程爱只。
異步函數(shù)源碼分析
主要的研究目標(biāo)是任務(wù)block是如何被調(diào)用的。
dispatch_async(queue_c, ^{
NSLog(@"12334");
});
-
dispatch_async
有兩個(gè)參數(shù)招刹,第一個(gè)參數(shù)是隊(duì)列恬试,第二個(gè)是任務(wù)(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
參數(shù)被傳入到_dispatch_continuation_init
函數(shù)中的第三個(gè)參數(shù),其余的地方?jīng)]有用到
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作為參數(shù)傳入
return _dispatch_continuation_init_f(dc, dqu, ctxt, func, flags, dc_flags);
}
-
work
被封裝成ctxt
和func
蔗喂,然后傳入到_dispatch_continuation_init_f
函數(shù)中
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);
}
- 到此我們看到了任務(wù)block的保存(dc->dc_ctxt = ctxt)和調(diào)用函數(shù)的保存(dc->dc_func = f)忘渔,那么在什么時(shí)候調(diào)用呢?我們就需要查看調(diào)用堆棧了
- 在任務(wù)block內(nèi)下斷點(diǎn)缰儿,然后
bt
命令查看調(diào)用棧畦粮。
void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
@try {
return f(ctxt);//之前保存的相關(guān)調(diào)用方法和任務(wù)
}
@catch (...) {
objc_terminate();
}
}
void
_dispatch_call_block_and_release(void *block)
{
void (^b)(void) = block;
b();
Block_release(b);
}
- 可以看到最后調(diào)用的是
_dispatch_call_block_and_release
函數(shù)。這個(gè)函數(shù)就是上面源碼中保存的f
乖阵,dc->dc_func = f;
- 此時(shí)就可以清楚為什么GCD相關(guān)的任務(wù)
block
不用我們手動(dòng)調(diào)用了宣赔。