在 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
加我微信溝通适肠。