[iOS][OC] 線程安全的可變數(shù)組、可變字典

在 iOS Objective-C 開發(fā)中,可變數(shù)組或字典 NSMutableArray/NSMutableDictionary 不是線程安全的芜果,即在兩個(gè)或以上線程對(duì)內(nèi)部元素同時(shí)進(jìn)行寫入换薄、讀取玉雾、新增、刪除等操作時(shí)轻要,會(huì)出現(xiàn)異掣囱或者超出預(yù)期的結(jié)果(result is unexpected),而不可變數(shù)組 NSArray/NSDictionary 因其不可變性可以在多線程下進(jìn)行讀取冲泥。要滿足多線程下數(shù)組操作的需求驹碍,常用的解決方案是對(duì)可變數(shù)組進(jìn)行封裝并提供與可變數(shù)組同等的 API 方便訪問,下面以可變數(shù)組為線索進(jìn)行討論凡恍,可變字典的性質(zhì)是相同道理志秃。

YYKit 中 YYThreadSafeArray 的實(shí)現(xiàn)

YYKit/Utility 中實(shí)現(xiàn)了線程安全的可變數(shù)組/字典,其實(shí)現(xiàn)的思路是:
① 將 NSMutableArray 對(duì)象作為成員封裝為一個(gè)新的類 YYThreadSafeArray
② 持有一個(gè)信號(hào)量對(duì)象作為數(shù)組操作的加鎖控制

@implementation YYThreadSafeArray {
    NSMutableArray *_arr;  //Subclass a class cluster...
    dispatch_semaphore_t _lock;
}

③ 初始化時(shí)構(gòu)造內(nèi)部成員數(shù)組和信號(hào)量對(duì)象(使用宏定義實(shí)現(xiàn))

// 通過宏定義實(shí)現(xiàn)帶入外部代碼實(shí)現(xiàn)初始化方法
#define INIT(...) self = super.init; \
if (!self) return nil; \
__VA_ARGS__; \
if (!_arr) return nil; \
_lock = dispatch_semaphore_create(1); \
return self;
- (instancetype)init {
    INIT(_arr = [[NSMutableArray alloc] init]);
}

④ 在進(jìn)行修改和讀取等操作時(shí)進(jìn)行加鎖(使用宏定義實(shí)現(xiàn))

// 通過宏定義對(duì)代碼塊進(jìn)行加鎖操作
#define LOCK(...) dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \
__VA_ARGS__; \
dispatch_semaphore_signal(_lock);

// id obj = array[idx];
- (id)objectAtIndexedSubscript:(NSUInteger)idx {
    LOCK(id o = [_arr objectAtIndexedSubscript:idx]); return o;
}
// array[idx] = obj;
- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx {
    LOCK([_arr setObject:obj atIndexedSubscript:idx]);
}

關(guān)于信號(hào)量 dispatch_semaphore_t 是為了控制資源的訪問頻率使用嚼酝,在 YYKit 的 INIT 宏定義實(shí)現(xiàn)中使用的信號(hào)量初始值為 1浮还,在加鎖操作前等待信號(hào)量,使用 dispatch_semaphore_wait 當(dāng)信號(hào)量大于等于 1 時(shí)革半,減去 1 點(diǎn)信號(hào)值并開始執(zhí)行后面的代碼碑定,此時(shí)信號(hào)值為 0,其他線程訪問時(shí)沒有信號(hào)值會(huì)一直等待又官,直到此任務(wù)完成后 dispatch_semaphore_signal 函數(shù)會(huì)將信號(hào)值加 1延刘,其他線程的訪問得以繼續(xù),從而實(shí)現(xiàn)信號(hào)量加鎖的目的六敬。

由于讀寫操作都使用了同一個(gè)信號(hào)量進(jìn)行控制碘赖,可以得知此方案對(duì)可變數(shù)組的多線程操作是串行的,可以保證可變數(shù)組在多線程下訪問的安全,即所有對(duì)數(shù)組的讀寫操作都將是依次逐個(gè)進(jìn)行普泡,潛在的問題是:限制了數(shù)組的多線程讀取操作播掷。

可并行讀取的線程安全數(shù)組

多線程寫入和讀取的加鎖操作是必要的,如何在此基礎(chǔ)上實(shí)現(xiàn)多線程并行讀取操作撼班?為此可以將數(shù)組的操作區(qū)分為寫操作歧匈、讀操作,需要滿足以下要求:
① 在寫入時(shí)砰嘁,不能有其他讀寫操作
② 可以并行讀取
這些要求恰好可以使用 Dispatch Concurrent Queue + dispatch_async_barrier 加以實(shí)現(xiàn)件炉,在同樣的封裝可變數(shù)組為成員變量的思路之后:
① 在初始化時(shí),構(gòu)造一個(gè)并行隊(duì)列

@implementation ThreadSafeArray {
    NSMutableArray *_arr; 
    dispatch_queue_t _queue;
}

- (instancetype)init {
    ....
    _queue = dispatch_queue_create("unique name", DISPATCH_QUEUE_CONCURRENT);
    ...
}

② 對(duì)寫操作進(jìn)行并發(fā)限制
使用 dispatch_barrier_async/dispatch_barrier_sync 函數(shù)矮湘,確保兩點(diǎn):一是在執(zhí)行此任務(wù)之前隊(duì)列中其他任務(wù)已經(jīng)完成斟冕,二是此任務(wù)完成之前隊(duì)列中新增的任務(wù)不會(huì)執(zhí)行,達(dá)到 barrier 的目標(biāo)缅阳。

- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx {
    dispatch_barrier_async(_queue, ^{
        [_arr setObject:obj atIndexedSubscript:idx];
    });
}

③ 支持并發(fā)讀取磕蛇,使用 dispatch_sync 函數(shù)是將讀取對(duì)象的操作加入到 queue 中,同步 dispatch 任務(wù)可以阻塞當(dāng)前線程直到任務(wù)完成后成功獲取到對(duì)象十办,而因?yàn)樯鲜?barrier 機(jī)制的存在如果有寫入操作則要等到寫入操作完成后才能執(zhí)行秀撇,單純的讀取操作可以在 queue 中并行,不會(huì) barrier 隊(duì)列橘洞。
PS:使用 __block id o 修飾是為了在 block 內(nèi)修改 block 外的局部變量捌袜。

- (id)objectAtIndexedSubscript:(NSUInteger)idx {
    __block id o;
    dispatch_sync(_queue, ^{
        o = [_arr objectAtIndexedSubscript:idx]
    });
    return o;
}

Update 2020/01/22

對(duì)于讀寫的控制,可以使用 pthread_lock_rw 即讀寫鎖炸枣,在使用上語義更加清晰。后續(xù)會(huì)補(bǔ)上這部分的代碼弄唧。

參考資料

Apple Document - Thread Safe Summary
StackOverFlow - avoid-this-dangling-pointer-with-arc
StackOverFlow - whats-the-difference-between-the-atomic-and-nonatomic-attributes

加我微信溝通适肠。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市候引,隨后出現(xiàn)的幾起案子侯养,更是在濱河造成了極大的恐慌,老刑警劉巖澄干,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逛揩,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡麸俘,警方通過查閱死者的電腦和手機(jī)辩稽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來从媚,“玉大人逞泄,你說我怎么就攤上這事。” “怎么了喷众?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵各谚,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我到千,道長(zhǎng)昌渤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任憔四,我火速辦了婚禮膀息,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘加矛。我一直安慰自己履婉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布斟览。 她就那樣靜靜地躺著毁腿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪苛茂。 梳的紋絲不亂的頭發(fā)上已烤,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音妓羊,去河邊找鬼胯究。 笑死,一個(gè)胖子當(dāng)著我的面吹牛躁绸,可吹牛的內(nèi)容都是我干的裕循。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼净刮,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼剥哑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起淹父,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤株婴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后暑认,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體困介,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年蘸际,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了座哩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡捡鱼,死狀恐怖八回,靈堂內(nèi)的尸體忽然破棺而出酷愧,到底是詐尸還是另有隱情,我是刑警寧澤缠诅,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布溶浴,位于F島的核電站,受9級(jí)特大地震影響管引,放射性物質(zhì)發(fā)生泄漏士败。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一褥伴、第九天 我趴在偏房一處隱蔽的房頂上張望谅将。 院中可真熱鬧,春花似錦重慢、人聲如沸饥臂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽隅熙。三九已至,卻和暖如春核芽,著一層夾襖步出監(jiān)牢的瞬間囚戚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工轧简, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留驰坊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓哮独,卻偏偏與公主長(zhǎng)得像拳芙,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子皮璧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容