在學(xué)習(xí)dispatch_once原理過程中官边,發(fā)現(xiàn)了之前因?yàn)樾盘柫恳鸬目ㄗ≈骶€程的問題所在凤巨。
所以针炉,了解原理,絕對是提高自己的必備條件渠羞。
我們帶著兩個問題去看
1.單例為什么會造成死鎖斤贰。
2.濫用單例為什么會導(dǎo)致內(nèi)存不斷增加。
如果對dispatch_once的基礎(chǔ)原理還不了解次询,可以看上一篇文章腋舌。
帶著問題,我們還是先看dispatch_once_f這個函數(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.局部變量块饺,用于在遍歷鏈表過程中獲取每一個在鏈表上的更改請求的信號量
_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之后雌芽,會調(diào)用dispatch_atomic_maximally_synchronizing_barrier()
//宏函數(shù)授艰,在intel處理器上,這個函數(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. 如果這個時(shí)候tmp的next指針還沒更新完畢,就等待一會屉佳,提示cpu減少額外處理谷朝,提升性能,節(jié)省電力武花。
while (!tmp->dow_next)
{
_dispatch_hardware_pause();
}
// 11. 取出當(dāng)前的信號量圆凰,告訴等待者,這次更改請求完成了体箕,輪到下一個了
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. 遍歷每一個后續(xù)請求,如果狀態(tài)已經(jīng)是Done累铅,直接進(jìn)行下一個
// 同時(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)識幾個對象.
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有個印象娃兽,因?yàn)楹竺鏁谩?/p>
**1.dispatch_once_f(dispatch_once_t val, void ctxt, dispatch_function_t func)傳入了三個參數(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的另外一個分支(else分支)進(jìn)行等待。
5.tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);使其為DISPATCH_ONCE_DONE冲泥,即“完成”。
6.然后比較 tmp和&dow的值,如果這兩個相等凡恍,分支結(jié)束志秃。
7.如果 tmp和&dow的值不相等,為什么會不相等呢嚼酝。因?yàn)樵赽lock執(zhí)行過程中浮还,會有其他線程進(jìn)入到本函數(shù),我們可以看else后面的內(nèi)容闽巩,會形成一個信號量鏈表钧舌,(vval指向值變?yōu)樾盘柫挎湹念^部,鏈表的尾部為&dow)涎跨,在這時(shí)候洼冻,進(jìn)入分支1的while循環(huán)中,因?yàn)槲覀兦懊嬗绾埽瑂truct _dispatch_once_waiter_s dow = { NULL, 0 }; 撞牢,dow.dow_next為null,所以需要一直等待叔营,等待temp.dow_next有值才可以進(jìn)行后面的操作屋彪。然后分支1就會進(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)建了一個信號量婴谱,并把值賦值給dow.dow_sema.
dow.dow_sema = _dispatch_get_thread_semaphore();
然后進(jìn)入了一個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)部會構(gòu)建一個倆表來維護(hù),如果在block完成之前夜矗,有其它的調(diào)用者進(jìn)來泛范,則會把這些調(diào)用者放到一個waiter鏈表中。
waiter鏈表中的每個調(diào)用者會等待一個信號量(dow.dow_sema)紊撕。在block執(zhí)行完了后罢荡,除了將onceToken置為DISPATCH_ONCE_DONE外,還會去遍歷waiter鏈中的所有waiter对扶,拋出相應(yīng)的信號量区赵,以告知waiter們調(diào)用已經(jīng)結(jié)束了
上面的兩個問題。
死鎖如何形成浪南?
兩個類相互調(diào)用其單例方法時(shí)笼才,調(diào)用者TestA作為一個waiter,在等待TestB中的block完成逞泄,而TestB中block的完成依賴于TestA中單例函數(shù)的block的執(zhí)行完成患整,而TestA中的block想要完成還需要TestB中的block完成……兩個人都在相互等待對方的完成,這就成了一個死鎖喷众。
濫用單例的為什么會死鎖各谚。
如果在dispatch_once函數(shù)的block塊執(zhí)行期間,循環(huán)進(jìn)入自己的dispatch_once函數(shù)到千,會造成鏈表一直增長昌渤,同樣也會造成死鎖。(這里只是簡單的A->B->A->B->A這樣的循環(huán)憔四,也可以是A->A->A這樣的更加直接的循環(huán).
如果在block執(zhí)行期間膀息,多次進(jìn)入調(diào)用同類的dispatch_once函數(shù)(即單例函數(shù)),會導(dǎo)致整體鏈表無限增長了赵,造成永久性死鎖
我覺得這也就是之前潜支,坐那個直播中,用信號量來控制時(shí)柿汛,為什么會卡主冗酿,因?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)錯位置,在對應(yīng)著源碼貌笨,應(yīng)該可以看出問題所在弱判。