在學(xué)習(xí)dispatch_once原理過程中,發(fā)現(xiàn)了之前因?yàn)樾盘柫恳鸬目ㄗ≈骶€程的問題所在。
所以,了解原理,絕對是提高自己的必備條件。
我們帶著兩個(gè)問題去看
1.單例為什么會(huì)造成死鎖。
2.濫用單例為什么會(huì)導(dǎo)致內(nèi)存不斷增加。
如果對dispatch_once的基礎(chǔ)原理還不了解,可以看上一篇文章。
帶著問題,我們還是先看dispatch_once_f這個(gè)函數(shù)。
#include "internal.h"
#undef dispatch_once
#undef dispatch_once_f
struct _dispatch_once_waiter_s
{
volatile struct _dispatch_once_waiter_s *volatile dow_next;
_dispatch_thread_semaphore_t dow_sema;
};
#define DISPATCH_ONCE_DONE ((struct _dispatch_once_waiter_s *)~0l)
#ifdef __BLOCKS__
// 1.應(yīng)用程序調(diào)用的入口
void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
struct Block_basic *bb = (void *)block;
// 2. 內(nèi)部邏輯
dispatch_once_f(val, block, (void *)bb->Block_invoke);
}
#endif
DISPATCH_NOINLINE
void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
struct _dispatch_once_waiter_s * volatile *vval =
(struct _dispatch_once_waiter_s**)val;
// 3. 類似于簡單的哨兵位
struct _dispatch_once_waiter_s dow = { NULL, 0 };
// 4. 在Dispatch_Once的block執(zhí)行期進(jìn)入的dispatch_once_t更改請求的鏈表
struct _dispatch_once_waiter_s *tail, *tmp;
// 5.局部變量,用于在遍歷鏈表過程中獲取每一個(gè)在鏈表上的更改請求的信號量
_dispatch_thread_semaphore_t sema;
// 6. Compare and Swap(用于首次更改請求)
if (dispatch_atomic_cmpxchg(vval, NULL, &dow))
{
dispatch_atomic_acquire_barrier();
// 7.調(diào)用dispatch_once的block
_dispatch_client_callout(ctxt, func);
//在寫入端,dispatch_once在執(zhí)行了block之后,會(huì)調(diào)用dispatch_atomic_maximally_synchronizing_barrier()
//宏函數(shù),在intel處理器上,這個(gè)函數(shù)編譯出的是cpuid指令。
dispatch_atomic_maximally_synchronizing_barrier();
//dispatch_atomic_release_barrier(); // assumed contained in above
// 8. 更改請求成為DISPATCH_ONCE_DONE(原子性的操作)
tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);
tail = &dow;
// 9. 發(fā)現(xiàn)還有更改請求,繼續(xù)遍歷
while (tail != tmp)
{
// 10. 如果這個(gè)時(shí)候tmp的next指針還沒更新完畢,就等待一會(huì),提示cpu減少額外處理,提升性能,節(jié)省電力。
while (!tmp->dow_next)
{
_dispatch_hardware_pause();
}
// 11. 取出當(dāng)前的信號量,告訴等待者,這次更改請求完成了,輪到下一個(gè)了
sema = tmp->dow_sema;
tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
_dispatch_thread_semaphore_signal(sema);
}
} else
{
// 12. 非首次請求,進(jìn)入此邏輯塊
dow.dow_sema = _dispatch_get_thread_semaphore();
// 13. 遍歷每一個(gè)后續(xù)請求,如果狀態(tài)已經(jīng)是Done,直接進(jìn)行下一個(gè)
// 同時(shí)該狀態(tài)檢測還用于避免在后續(xù)wait之前,信號量已經(jīng)發(fā)出(signal)造成
// 的死鎖
for (;;)
{
tmp = *vval;
if (tmp == DISPATCH_ONCE_DONE)
{
break;
}
dispatch_atomic_store_barrier();
// 14. 如果當(dāng)前dispatch_once執(zhí)行的block沒有結(jié)束,那么就將這些
// 后續(xù)請求添加到鏈表當(dāng)中
if (dispatch_atomic_cmpxchg(vval, tmp, &dow))
{
dow.dow_next = tmp;
_dispatch_thread_semaphore_wait(dow.dow_sema);
}
}
_dispatch_put_thread_semaphore(dow.dow_sema);
}
}
首先我們先來認(rèn)識幾個(gè)對象.
struct _dispatch_once_waiter_s
{
volatile struct _dispatch_once_waiter_s *volatile dow_next;
_dispatch_thread_semaphore_t dow_sema;
};
struct _dispatch_once_waiter_s dow = { NULL, 0 };
要對dow.dow_next有個(gè)印象,因?yàn)楹竺鏁?huì)用。
**1.dispatch_once_f(dispatch_once_t val, void ctxt, dispatch_function_t func)傳入了三個(gè)參數(shù)ctxt是外部傳入的block的指針,func是block里具體執(zhí)行的函數(shù)。
2. dispatch_atomic_cmpxchg 是原子交換函數(shù),dispatch_atomic_cmpxchg(vval, NULL, &dow)也就是吧vval的值賦值給&dow.
3. _dispatch_client_callout(ctxt, func);根據(jù)ctxt找到block,并執(zhí)行block中的函數(shù)。
4. dispatch_atomic_maximally_synchronizing_barrier函數(shù)的作用,是可以讓其他線程來讀取到未初始化的對象,從而可以使這些線程進(jìn)入dispatch_once_f的另外一個(gè)分支(else分支)進(jìn)行等待。
5.tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);使其為DISPATCH_ONCE_DONE,即“完成”。
6.然后比較 tmp和&dow的值,如果這兩個(gè)相等,分支結(jié)束。
7.如果 tmp和&dow的值不相等,為什么會(huì)不相等呢。因?yàn)樵赽lock執(zhí)行過程中,會(huì)有其他線程進(jìn)入到本函數(shù),我們可以看else后面的內(nèi)容,會(huì)形成一個(gè)信號量鏈表,(vval指向值變?yōu)樾盘柫挎湹念^部,鏈表的尾部為&dow),在這時(shí)候,進(jìn)入分支1的while循環(huán)中,因?yàn)槲覀兦懊?,struct _dispatch_once_waiter_s dow = { NULL, 0 }; ,dow.dow_next為null,所以需要一直等待,等待temp.dow_next有值才可以進(jìn)行后面的操作。然后分支1就會(huì)進(jìn)行等待分支2的進(jìn)行,只有當(dāng)分支2的dow_dow_next = tmp被執(zhí)行了,才可以繼續(xù)往后面執(zhí)行。
while (!tmp->dow_next)
{
_dispatch_hardware_pause();
}
8.我們仔細(xì)看下分支2的操作。
創(chuàng)建了一個(gè)信號量,并把值賦值給dow.dow_sema.
dow.dow_sema = _dispatch_get_thread_semaphore();
然后進(jìn)入了一個(gè)for循環(huán)中,如果vval的值已經(jīng)為DISPATCH_ONCE_DONE,則直接break。
如果vval的值不為DISPATCH_ONCE_DONE,則把vval賦值給&dow.此時(shí)val.dow_next還是為null,把dow.dow_next = tmp來增加鏈表的節(jié)點(diǎn),解決了分支1中while進(jìn)行等待的問題。
if (dispatch_atomic_cmpxchg(vval, tmp, &dow))
{
dow.dow_next = tmp;
_dispatch_thread_semaphore_wait(dow.dow_sema);
}
然后等待在信號量上,當(dāng)block執(zhí)行分支1完成并遍歷鏈表來signal時(shí),喚醒、釋放信號量,然后一切就完成了。
分支1的while循環(huán),需要等待分支2的 dow.dow_next = tmp;賦值,然后,分支2的 _dispatch_thread_semaphore_wait(dow.dow_sema);需要等待分支1的_dispatch_thread_semaphore_signal(sema);。
總結(jié)下上面的問題。
dispatch_once實(shí)際上內(nèi)部會(huì)構(gòu)建一個(gè)倆表來維護(hù),如果在block完成之前,有其它的調(diào)用者進(jìn)來,則會(huì)把這些調(diào)用者放到一個(gè)waiter鏈表中。
waiter鏈表中的每個(gè)調(diào)用者會(huì)等待一個(gè)信號量(dow.dow_sema)。在block執(zhí)行完了后,除了將onceToken置為DISPATCH_ONCE_DONE外,還會(huì)去遍歷waiter鏈中的所有waiter,拋出相應(yīng)的信號量,以告知waiter們調(diào)用已經(jīng)結(jié)束了
上面的兩個(gè)問題。
死鎖如何形成?
兩個(gè)類相互調(diào)用其單例方法時(shí),調(diào)用者TestA作為一個(gè)waiter,在等待TestB中的block完成,而TestB中block的完成依賴于TestA中單例函數(shù)的block的執(zhí)行完成,而TestA中的block想要完成還需要TestB中的block完成……兩個(gè)人都在相互等待對方的完成,這就成了一個(gè)死鎖。
濫用單例的為什么會(huì)死鎖。
如果在dispatch_once函數(shù)的block塊執(zhí)行期間,循環(huán)進(jìn)入自己的dispatch_once函數(shù),會(huì)造成鏈表一直增長,同樣也會(huì)造成死鎖。(這里只是簡單的A->B->A->B->A這樣的循環(huán),也可以是A->A->A這樣的更加直接的循環(huán).
如果在block執(zhí)行期間,多次進(jìn)入調(diào)用同類的dispatch_once函數(shù)(即單例函數(shù)),會(huì)導(dǎo)致整體鏈表無限增長,造成永久性死鎖
我覺得這也就是之前,坐那個(gè)直播中,用信號量來控制時(shí),為什么會(huì)卡主,因?yàn)槲矣脝卫庋b的信號量,然后單例循環(huán)調(diào)用,發(fā)生了死鎖。
2021.8.10 補(bǔ)充一下死鎖的demo
#import "ShareA.h"
#import "ShareB.h"
@implementation ShareA
+(instancetype)instance {
static ShareA *a;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[[ShareB instance] test];
a = [[ShareA alloc]init];
});
return a;
}
- (void)test {
NSLog(@"ShareA");
}
@end
#import "ShareB.h"
#import "ShareA.h"
@implementation ShareB
+(instancetype)instance {
static ShareB *a;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[[ShareA instance]test];
a = [[ShareB alloc]init];
});
return a;
}
- (void)test {
NSLog(@"ShareB");
}
@end


通過下面的報(bào)錯(cuò)位置,在對應(yīng)著源碼,應(yīng)該可以看出問題所在。