本文整理自《Effective Objective-C 2.0》风范,通過(guò)分析比較不同的同步鎖的優(yōu)缺點(diǎn),使用GCD方法一步步找到更高效的同步鎖沪么。
在Objective-C中硼婿,如果有多個(gè)線程要執(zhí)行同一份代碼,那么這時(shí)就會(huì)出現(xiàn)線程安全問題禽车。首先寇漫,我們看下什么時(shí)候線程安全問題。
線程安全
如果一段代碼所在的進(jìn)程中有多個(gè)線程在同時(shí)運(yùn)行殉摔,那么這些線程就有可能會(huì)同時(shí)運(yùn)行這段代碼州胳。假如多個(gè)線程每次運(yùn)行結(jié)果和單線程運(yùn)行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣的钦勘,就是線程安全的陋葡。
由于可讀寫的全局變量及靜態(tài)變量(在 Objective-C 中還包括屬性和實(shí)例變量)可以在不同線程修改,所以這兩者也通常是引起線程安全問題的所在彻采。
Objective-C中的同步鎖
在 Objective-C 中腐缤,如果有多個(gè)線程執(zhí)行同一份代碼,那么有可能會(huì)出現(xiàn)線程安全問題肛响。這種情況下岭粤,就需要使用所來(lái)實(shí)現(xiàn)某種同步機(jī)制。
在 GCD出現(xiàn)之前特笋,有兩種方法剃浇,一種采用的是內(nèi)置的“同步塊”(synchronization block)巾兆,另一種方法是使用鎖對(duì)象。
同步塊(synchronization block)
- (void)synchronizedMethod {
@synchronized (self) {
//Safe
}
}
這種寫法會(huì)根據(jù)給定的對(duì)象虎囚,自動(dòng)創(chuàng)建一個(gè)鎖角塑,并等待塊中的代碼執(zhí)行完畢。執(zhí)行到這段代碼結(jié)尾處淘讥,鎖就釋放了圃伶。
該同步方法的優(yōu)點(diǎn)就是我們不需要在代碼中顯式的創(chuàng)建鎖對(duì)象,便可以實(shí)現(xiàn)鎖的機(jī)制蒲列。然而窒朋,濫用@synchronized (self)則會(huì)降低代碼效率,因?yàn)楣猛粋€(gè)鎖的那些同步塊蝗岖,都必須按順序執(zhí)行侥猩。若是在self對(duì)象上頻繁加鎖,程序可能要等另一段與此無(wú)關(guān)的代碼執(zhí)行完畢抵赢,才能繼續(xù)執(zhí)行當(dāng)前代碼欺劳,這樣效率就低了。
注:因?yàn)锧synchronized (self)方法針對(duì)self只有一個(gè)鎖瓣俯,相當(dāng)于對(duì)于self的所有用到同步塊的地方都是公用同一個(gè)鎖杰标,所以如果有多個(gè)同步塊,則其他的同步塊都要等待當(dāng)前同步塊執(zhí)行完畢才能繼續(xù)執(zhí)行彩匕。
- (void)synchronizedAMethod {
@synchronized (self) {
//Safe
}
}
- (void)synchronizedBMethod {
@synchronized (self) {
//Safe
}
}
- (void)synchronizedCMethod {
@synchronized (self) {
//Safe
}
}
以上代碼腔剂,如果當(dāng)前synchronizedAMethod
方法正在執(zhí)行,則synchronizedBMethod
和synchronizedCMethod
方法需要等待synchronizedAMethod
完畢后才能執(zhí)行驼仪,不能達(dá)到并發(fā)的效果掸犬。
鎖對(duì)象
@property (nonatomic,strong) NSLock *lock;
_lock = [[NSLock alloc] init];
- (void)synchronizedMethod {
[_lock lock];
//Safe
[_lock unlock];
}
以上是簡(jiǎn)單鎖對(duì)象的實(shí)現(xiàn)方式,但是如果鎖使用不當(dāng)绪爸,會(huì)出現(xiàn)死鎖現(xiàn)象湾碎,這時(shí)可以使用NSRecursiveLock這種“遞歸鎖”(recursive lock)。
除了以上鎖對(duì)象奠货,還有NSConditionLock 條件鎖 介褥、NSDistributedLock 分布式鎖 ,這些適用于不同的場(chǎng)景递惋,這里就不展開說(shuō)了柔滔。
以上這些鎖,使用的時(shí)候還是有缺陷的萍虽。在極端情況下睛廊,同步塊會(huì)導(dǎo)致死鎖,另外杉编,效率也不見得高超全,而如果直接使用鎖對(duì)象的話咆霜,一旦遇到死鎖,就會(huì)非常麻煩嘶朱。
GCD鎖
在開始說(shuō)GCD鎖之前蛾坯,我們先了解一下GCD的中的任務(wù)派發(fā)和隊(duì)列。
任務(wù)派發(fā)
任務(wù)派發(fā)方式 | 說(shuō)明 |
---|---|
dispatch_sync() | 同步執(zhí)行疏遏,完成了它預(yù)定的任務(wù)后才返回偿衰,阻塞當(dāng)前線程 |
dispatch_async() | 異步執(zhí)行,會(huì)立即返回改览,預(yù)定的任務(wù)會(huì)完成但不會(huì)等它完成,不阻塞當(dāng)前線程 |
隊(duì)列種類
隊(duì)列種類 | 說(shuō)明 |
---|---|
串行隊(duì)列 | 每次只能執(zhí)行一個(gè)任務(wù)缤言,并且必須等待前一個(gè)執(zhí)行任務(wù)完成 |
并發(fā)隊(duì)列 | 一次可以并發(fā)執(zhí)行多個(gè)任務(wù)宝当,不必等待執(zhí)行中的任務(wù)完成 |
GCD隊(duì)列種類
GCD隊(duì)列種類 | 獲取方法 | 隊(duì)列類型 | 說(shuō)明 |
---|---|---|---|
主隊(duì)列 | dispatch_get_main_queue | 串行隊(duì)列 | 主線中執(zhí)行 |
全局隊(duì)列 | dispatch_get_global_queue | 并發(fā)隊(duì)列 | 子線程中執(zhí)行 |
用戶隊(duì)列 | dispatch_queue_create | 串并都可以 | 子線程中執(zhí)行 |
以前同步鎖的實(shí)現(xiàn)方式
在Objective-C中,屬性就是開發(fā)者經(jīng)常需要同步的地方胆萧。通常開發(fā)者想省事的話(以前我也是這樣覺得)庆揩,會(huì)這樣寫:
- (NSString *)someString {
@synchronized (self) {
return _someString;
}
}
- (void)setSomeString:(NSString *)someString {
@synchronized (self) {
_someString = someString;
}
}
以上代碼除了上文提到的效率低以外,還有一個(gè)問題跌穗,就是該方法并不能保證訪問該對(duì)象時(shí)絕對(duì)是線程安全的订晌。雖然,這種方法在訪問屬性時(shí)蚌吸,確實(shí)是“原子”的锈拨,也必定能從中獲取到有效值,然而在同一線程上多次調(diào)用getter方法羹唠,每次獲取到的結(jié)果未必相同奕枢。在兩次訪問操作之間,其他線程可能會(huì)寫入新的屬性值佩微。此時(shí)缝彬,只能保證讀寫操作是“原子”的,而多個(gè)線程的執(zhí)行順序哺眯,我們沒有辦法控制谷浅。
使用GCD串行隊(duì)列來(lái)實(shí)現(xiàn)同步鎖
有種簡(jiǎn)單而高效的方法可以替代同步塊或鎖對(duì)象,那就是使用“串行同步隊(duì)列”奶卓。將讀取操作以及寫入操作都安排在同一個(gè)隊(duì)列里一疯,即可保證數(shù)據(jù)同步。
用法如下:
@property (nonatomic,strong) dispatch_queue_t syncQueue;
_syncQueue = dispatch_queue_create("com.effetiveobjectivec.syncQueue", NULL);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return _someString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_sync(_syncQueue, ^{
_someString = someString;
});
}
此模式的思路是:把設(shè)置操作與獲取操作都安排在序列化的隊(duì)列里執(zhí)行寝杖,這樣的話违施,所有針對(duì)屬性的訪問操作就都同步了。
注:getter方法中瑟幕,用一個(gè)臨時(shí)變量來(lái)保存值磕蒲,是因?yàn)樵赽lock中return的話留潦,只是return到block中了,沒有真正返回到對(duì)應(yīng)的getter方法中辣往,而__block是為了可以在block中改變改臨時(shí)變量而用兔院。
雖然問題解決了,但是我們還可以進(jìn)一步優(yōu)化站削。設(shè)置方法不一定非得是同步的坊萝。設(shè)置實(shí)例變量所用的塊,并不需要向設(shè)置方法返回什么值许起。那代碼可以改成:
- (void)setSomeString:(NSString *)someString {
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
這次把同步改成了異步十偶,也許看來(lái),這樣改動(dòng)园细,性能是會(huì)有提升的惦积,但是你測(cè)一下程序的性能,可能會(huì)發(fā)現(xiàn)這種寫法比原來(lái)慢猛频。因?yàn)閳?zhí)行異步派發(fā)時(shí)狮崩,是需要拷貝塊。若拷貝塊所用的時(shí)間明顯超過(guò)執(zhí)行塊所需的時(shí)間鹿寻,則這種做法將比原來(lái)的更慢睦柴。
注:本例子代碼比較簡(jiǎn)單,若是要執(zhí)行的塊代碼邏輯比較復(fù)雜的話毡熏,那么該寫法可能還是比原來(lái)的塊些
使用GCD并發(fā)隊(duì)列來(lái)實(shí)現(xiàn)同步鎖
對(duì)于屬性的讀寫坦敌,我們希望多個(gè)獲取方法可以并發(fā)執(zhí)行,而獲取方法與設(shè)置方法之間不能并發(fā)執(zhí)行招刹,利用這個(gè)特點(diǎn)恬试,還能寫出更快一些的代碼來(lái)。此時(shí)正可體現(xiàn)出GCD的好處疯暑。而用同步鎖或鎖對(duì)象训柴,是無(wú)法輕易實(shí)現(xiàn)下面這種方案的。這次我們使用并發(fā)隊(duì)列:
@property (nonatomic,strong) dispatch_queue_t syncQueue;
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
以上代碼妇拯,還無(wú)法正確實(shí)現(xiàn)同步幻馁。因?yàn)樗凶x寫操作都會(huì)在同一個(gè)隊(duì)列上執(zhí)行,而該隊(duì)列是并發(fā)隊(duì)列越锈,所有讀取和寫入操作都可以隨時(shí)執(zhí)行仗嗦,沒有達(dá)到同步效果。此問題我們可以通過(guò)一個(gè)簡(jiǎn)單的GCD功能解決--柵欄(barrier)甘凭。下列函數(shù)可以向隊(duì)列中派發(fā)塊稀拐,將其作為柵欄使用:
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
注:dispatch_barrier_async如果傳入自己創(chuàng)建的并行隊(duì)列時(shí),會(huì)阻塞當(dāng)前隊(duì)列執(zhí)行丹弱,而不阻塞當(dāng)前線程德撬。
dispatch_barrier_sync如果傳入自己創(chuàng)建的并行隊(duì)列時(shí)铲咨,阻塞當(dāng)前隊(duì)列的同時(shí)也會(huì)阻塞當(dāng)前線程,請(qǐng)注意
并發(fā)隊(duì)列如果發(fā)現(xiàn)接下來(lái)要處理的塊是個(gè)柵欄塊蜓洪,那么就一直要等當(dāng)前所有并發(fā)塊都執(zhí)行完畢纤勒,才會(huì)單獨(dú)執(zhí)行這個(gè)柵欄塊。這待柵欄塊執(zhí)行完畢隆檀,在按正常方式繼續(xù)向下處理摇天。這樣就解決了并發(fā)隊(duì)列的同步問題。
GCD并發(fā)隊(duì)列中加入柵欄
本例中恐仑,可以用柵欄塊來(lái)實(shí)現(xiàn)屬性的設(shè)置方法泉坐。在設(shè)置方法中使用了柵欄塊之后,對(duì)屬性的讀取操作依然可以并發(fā)執(zhí)行裳仆,但寫入操作卻必須單獨(dú)執(zhí)行了坚冀。在下圖中演示的這個(gè)隊(duì)列中,有多個(gè)讀取操作鉴逞,而且還有一個(gè)寫入操作。
實(shí)現(xiàn)代碼很簡(jiǎn)單:
@property (nonatomic,strong) dispatch_queue_t syncQueue;
//_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/* 這里應(yīng)該使用自己創(chuàng)建的并發(fā)隊(duì)列勾徽,因?yàn)樘O果文檔中指出,如果使用的是全局隊(duì)列或者創(chuàng)建的不是并發(fā)隊(duì)列统扳,
則dispatch_barrier_async實(shí)際上就相當(dāng)于dispatch_async喘帚,就達(dá)不到我們想要的效果了 */
_syncQueue = dispatch_queue_create("com.effetiveobjectivec.syncQueue", DISPATCH_QUEUE_CONCURRENT);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
測(cè)試一下性能,你就會(huì)發(fā)現(xiàn)咒钟,這種做法肯定比使用串行隊(duì)列要快吹由。其中,設(shè)置函數(shù)也可以改用同步柵欄塊來(lái)實(shí)現(xiàn)朱嘴,那樣做可能會(huì)更高效倾鲫,其原因之前已經(jīng)解釋過(guò)了——這里就要權(quán)衡拷貝塊的時(shí)間和塊執(zhí)行時(shí)間了