使用三方庫CMVoipMulticastDelegate過程中糟把,發(fā)現(xiàn)此庫并沒有添加線程保護(hù)相關(guān)邏輯,會(huì)導(dǎo)致對(duì)delegateNodes操作時(shí)產(chǎn)生crash:Collection <__NSArrayM: 0xb550c30> was mutated while being enumerated惜傲, 這時(shí)由于在枚舉delegateNodes時(shí),恰好有對(duì)這個(gè)數(shù)組的操作缓艳,如增刪改等槽奕。
最懶的操作就是每次枚舉之前copy一份嘴纺,使用copy出來的數(shù)組進(jìn)行枚舉败晴,但這個(gè)方法存在數(shù)據(jù)錯(cuò)誤風(fēng)險(xiǎn)。最好的方式當(dāng)然是進(jìn)行線程安全的處理栽渴,基于性能考慮尖坤,我使用了信號(hào)量來處理。但是卻在其中引入錯(cuò)誤闲擦,初始 ** 錯(cuò)誤代碼 ** 如下:
@implementation CMVoipMulticastDelegate
- (id)init
{
if ((self = [super init]))
{
delegateNodes = [[NSMutableArray alloc] init];
signal = dispatch_semaphore_create(1);
overTime = dispatch_time(DISPATCH_TIME_NOW, DISPATCH_TIME_FOREVER);//DISPATCH_TIME_FOREVER;
serialQueue = dispatch_queue_create("com.hycmcc.CMVoipMulticastDelegate", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)semaphore_signal_in_global_queue
{
// signal始終放在一個(gè)線程中糖驴,一是保證不會(huì)因?yàn)閣ait和signal在一個(gè)線程導(dǎo)致死鎖僚祷,二是避免過多新建線程造成資源浪費(fèi)
dispatch_async(serialQueue, ^{
dispatch_semaphore_signal(signal);
});
}
- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue
{
if (delegate == nil) return;
if (delegateQueue == NULL) delegateQueue = dispatch_get_main_queue();
CMVoipMulticastDelegateNode *node =
[[CMVoipMulticastDelegateNode alloc] initWithDelegate:delegate delegateQueue:delegateQueue];
dispatch_semaphore_wait(signal, overTime);
CMVoIPLogInfo(@"CMVoipMulticastDelegate addDelegate");
[delegateNodes addObject:node];
[self semaphore_signal_in_global_queue];
}
這份代碼在調(diào)試時(shí),還是會(huì)出現(xiàn)上面的crash贮缕,于是需要對(duì)其調(diào)試定位問題所在。
首先考慮肯定是線程鎖沒有起作用俺榆,那就斷點(diǎn)調(diào)試一下:
通過xcode斷點(diǎn)感昼,并在下面觀察區(qū),點(diǎn)擊signal屬性罐脊,右鍵選擇“print description of signa” 定嗓,可以在控制臺(tái)打印signal的值∑甲溃或者通過lldb指令:
(lldb) po signal
<OS_dispatch_semaphore: semaphore[0x1c02892e0] = { xref = 1, ref = 1, port = 0x25351, value = 0, orig = 1 }>
觀察到這個(gè)打印的值宵溅,那現(xiàn)在可以考慮是可以將其中這幾個(gè)值都分別打印出來,或者判斷其中某一個(gè)值上炎,超過1恃逻,說明線程數(shù)字超過1了,就是問題出現(xiàn)了藕施。
但是這個(gè)OS_dispatch_semaphore是底層c的數(shù)據(jù)結(jié)構(gòu)寇损,無法通過屬性直接獲取,或者通過結(jié)構(gòu)體->形式引用到其中的值裳食。 所以只能通過打印這個(gè)OS_dispatch_semaphore矛市,通過日志來分析(單步debug很難復(fù)現(xiàn)多線程問題)。
所以就添加了打印語句:
CMVoIPLogInfo(@"CMVoipMulticastDelegate signal:%@", signal);
結(jié)果如下:
<OS_dispatch_semaphore: semaphore[0x1c02892e0]>
可見其中并沒有{ xref = 1, ref = 1, port = 0x25351, value = 0, orig = 1 } 數(shù)據(jù)結(jié)構(gòu)中具體元素的值诲祸,難道是需要使用description 浊吏?
更改打印語句如下:
CMVoIPLogInfo(@"CMVoipMulticastDelegate signal:%@", signal.description);
結(jié)果如下:
<OS_dispatch_semaphore: semaphore[0x1c02892e0]>
怎么還是這樣!
按理說救氯,lldb的調(diào)試時(shí)的上下文和打印語句應(yīng)該是一樣的找田,應(yīng)該可以拿到同樣的數(shù)據(jù),為什么打印不出來同樣的值呢径密? 那我們就嘗試下是不是有其他屬性可以將斷點(diǎn)時(shí)的值復(fù)現(xiàn)出來午阵,當(dāng)我們隨便輸入一個(gè)字母d時(shí),可以看到聯(lián)想的屬性列表:
在description屬性下面還有一個(gè)debugDescription屬性享扔,嘗試一下結(jié)果如下:
<OS_dispatch_semaphore: semaphore[0x1c02892e0] = { xref = 1, ref = 1, port = 0x25351, value = 0, orig = 1 }>
原來debugDescription屬性時(shí)專門用來調(diào)試的底桂,iOS系統(tǒng)的對(duì)象數(shù)據(jù)結(jié)構(gòu)圖設(shè)計(jì)的還真是不錯(cuò),所以我們?nèi)粘i_發(fā)時(shí)惧眠,一些類或者model設(shè)計(jì)時(shí)籽懦,為了便于調(diào)試,也應(yīng)該分別重寫debugDescription和description兩個(gè)屬性氛魁。
這時(shí)暮顺,我們就可以在合適的地方添加log語句來看問題了厅篓,日志精簡如下:
2018-07-09 14:32:18:260 cMeeting[41681:4166517] CMVoipMulticastDelegate semaphore+1 before <OS_dispatch_semaphore: semaphore[0x1c02897e0] = { xref = 1, ref = 1, port = 0x25351, value = 0, orig = 1 }>
2018-07-09 14:32:18:260 cMeeting[41681:4166517] CMVoipMulticastDelegate semaphore+1 after <OS_dispatch_semaphore: semaphore[0x1c02897e0] = { xref = 1, ref = 1, port = 0x25351, value = 1, orig = 1 }>
2018-07-09 14:32:18:358 cMeeting[41681:4166880] CMVoipMulticastDelegate semaphore-1 before <OS_dispatch_semaphore: semaphore[0x1c02897e0] = { xref = 1, ref = 1, port = 0x25351, value = 2, orig = 1 }>
可以看到,value是當(dāng)前線程數(shù)捶码,當(dāng)出現(xiàn)大于1 的情況羽氮,就說明問題出現(xiàn)了(我們已經(jīng)設(shè)置了最大并發(fā)數(shù)是1)。
我們這時(shí)首先判斷是不是重復(fù)調(diào)用了dispatch_semaphore_signal方法惫恼,導(dǎo)致釋放了多余信號(hào)档押,例如釋放了兩個(gè)信號(hào)的話,就會(huì)導(dǎo)致兩個(gè)在等待dispatch_semaphore_wait的線程同時(shí)獲得信號(hào)祈纯,而進(jìn)入相關(guān)操作代碼中令宿,導(dǎo)致crash。
經(jīng)過代碼走查腕窥,和日志分析粒没,并沒有這種情況。
經(jīng)過日志和debug調(diào)試簇爆,我們發(fā)現(xiàn)在出現(xiàn)value = 1癞松,即已經(jīng)有一個(gè)線程獲得信號(hào)之后,其他線程在dispatch_semaphore_wait方法處沒有阻塞等待冕碟,而是直接開始執(zhí)行后面代碼拦惋,也就是說這里信號(hào)量確實(shí)沒有起到限制并發(fā)線程數(shù)目的作用。
那么問題在哪呢安寺?
是超時(shí)時(shí)間沒起作用魂奥? 還是信號(hào)量使用有問題仆潮?
經(jīng)過查閱其他人使用信號(hào)量的方式,驚人的發(fā)現(xiàn),overTime這里雖然要求是dispatch_time 類型跷叉,但是正確的卻是直接使用DISPATCH_TIME_FOREVER咱扣,而不是如上面所示的dispatch_time(DISPATCH_TIME_NOW, DISPATCH_TIME_FOREVER);
查看dispatch的源碼也能判斷到這里的錯(cuò)誤所在:
dispatch_time_t
dispatch_time(dispatch_time_t inval, int64_t delta)
{
if (inval == DISPATCH_TIME_FOREVER) {
return DISPATCH_TIME_FOREVER;
}
if ((int64_t)inval < 0) {
// wall clock
if (delta >= 0) {
if ((int64_t)(inval -= delta) >= 0) {
return DISPATCH_TIME_FOREVER; // overflow
}
return inval;
}
if ((int64_t)(inval -= delta) >= -1) {
// -1 is special == DISPATCH_TIME_FOREVER == forever
return -2; // underflow
}
return inval;
}
// mach clock
delta = _dispatch_time_nano2mach(delta);
if (inval == 0) {
inval = mach_absolute_time();
}
if (delta >= 0) {
if ((int64_t)(inval += delta) <= 0) {
return DISPATCH_TIME_FOREVER; // overflow
}
return inval;
}
if ((int64_t)(inval += delta) < 1) {
return 1; // underflow
}
return inval;
}
這個(gè)函數(shù)邏輯比較簡單硝烂,當(dāng)我們錯(cuò)誤的使用dispatch_time(DISPATCH_TIME_NOW, DISPATCH_TIME_FOREVER);這種形式時(shí)别威,對(duì)應(yīng)函數(shù)的入?yún)nval 和 delta分別是DISPATCH_TIME_NOW和DISPATCH_TIME_FOREVER,而二者宏定義的值為:
#define DISPATCH_TIME_NOW 0
#define DISPATCH_TIME_FOREVER (~0ull)
所以此函數(shù)就會(huì)經(jīng)下面邏輯將inval返回凳枝,
if (inval == 0) {
inval = mach_absolute_time();
}
也就是說返回的就是mach_absolute_time()抄沮,獲取到的是當(dāng)前系統(tǒng)的時(shí)間。 信號(hào)量在判斷返回的時(shí)間時(shí)判斷到此時(shí)刻岖瑰,發(fā)現(xiàn)已經(jīng)過去了叛买,那么直接就不等待而直接執(zhí)行下面的代碼,從而沒有實(shí)現(xiàn)阻塞的目的蹋订。
所以從這個(gè)函數(shù)我們也能看出率挣,我們正確的使用方式應(yīng)該是:
dispatch_time(DISPATCH_TIME_FOREVER, DISPATCH_TIME_NOW);
這樣會(huì)通過下面的邏輯:
if (inval == DISPATCH_TIME_FOREVER) {
return DISPATCH_TIME_FOREVER;
}
直接返回DISPATCH_TIME_FOREVER,從而得到了我們期望的值露戒。這里其實(shí)第二個(gè)參數(shù)就沒有實(shí)際作用了椒功。
總結(jié):
信號(hào)量直接打印%@捶箱,無法顯示已經(jīng)開啟的信號(hào)數(shù)量,因?yàn)榇藭r(shí)使用的是x.description动漾, 并且比xcode的debug斷點(diǎn)時(shí)看到的信息少丁屎,此時(shí)應(yīng)該使用x.debugDescription,此時(shí)的顯示信息即斷點(diǎn)可查看的信息相符
通過打印信號(hào)量谦炬,查看其中value值可以判斷信號(hào)數(shù)量悦屏,當(dāng)creat為1時(shí),最多value只能為1键思,如果有2的情況,說明未起作用
當(dāng)信號(hào)量未起作用甫贯,可以從兩方面考慮:
超時(shí)時(shí)間設(shè)置錯(cuò)誤吼鳞,例如很短的超時(shí)時(shí)間,或者參數(shù)傳遞的是dispatch_time變量其實(shí)就是0.
是否有重復(fù)signal的地方叫搁,即與wait不配對(duì)信號(hào)量引起死鎖:當(dāng)后面進(jìn)來wait信號(hào)量的線程與前面已經(jīng)等到信號(hào)但還未signal的signal所處是同一個(gè)線程時(shí)赔桌,由于wait阻塞了線程,導(dǎo)致signal一直無法進(jìn)入渴逻,一直無法釋放疾党,wait死鎖在那。