Objective-C中的同步鎖
在 Objective-C 中倔幼,如果有多個(gè)線(xiàn)程執(zhí)行同一份代碼,那么有可能會(huì)出現(xiàn)線(xiàn)程安全問(wèn)題布蔗。這種情況下衅码,就需要使用所來(lái)實(shí)現(xiàn)某種同步機(jī)制拯刁。
在 GCD出現(xiàn)之前,有兩種方法肆良,一種采用的是內(nèi)置的“同步塊”(synchronization block)筛璧,另一種方法是使用鎖對(duì)象。
同步塊(synchronization block)
- (void)synchronizedMethod {@synchronized(self) {//Safe}}
這種寫(xiě)法會(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 = [[NSLockalloc] 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)景忿偷,這里就不展開(kāi)說(shuō)了金顿。
以上這些鎖,使用的時(shí)候還是有缺陷的鲤桥。在極端情況下揍拆,同步塊會(huì)導(dǎo)致死鎖,另外茶凳,效率也不見(jiàn)得高嫂拴,而如果直接使用鎖對(duì)象的話(huà),一旦遇到死鎖贮喧,就會(huì)非常麻煩筒狠。
GCD鎖
在開(kāi)始說(shuō)GCD鎖之前,我們先了解一下GCD的中的任務(wù)派發(fā)和隊(duì)列箱沦。
任務(wù)派發(fā)
任務(wù)派發(fā)方式說(shuō)明
dispatch_sync()同步執(zhí)行辩恼,完成了它預(yù)定的任務(wù)后才返回,阻塞當(dāng)前線(xiàn)程
dispatch_async()異步執(zhí)行谓形,會(huì)立即返回灶伊,預(yù)定的任務(wù)會(huì)完成但不會(huì)等它完成,不阻塞當(dāng)前線(xiàn)程
隊(duì)列種類(lèi)
隊(duì)列種類(lèi)說(shuō)明
串行隊(duì)列每次只能執(zhí)行一個(gè)任務(wù)寒跳,并且必須等待前一個(gè)執(zhí)行任務(wù)完成
并發(fā)隊(duì)列一次可以并發(fā)執(zhí)行多個(gè)任務(wù)聘萨,不必等待執(zhí)行中的任務(wù)完成
GCD隊(duì)列種類(lèi)
GCD隊(duì)列種類(lèi)獲取方法隊(duì)列類(lèi)型說(shuō)明
主隊(duì)列dispatch_get_main_queue串行隊(duì)列主線(xiàn)中執(zhí)行
全局隊(duì)列dispatch_get_global_queue并發(fā)隊(duì)列子線(xiàn)程中執(zhí)行
用戶(hù)隊(duì)列dispatch_queue_create串并都可以子線(xiàn)程中執(zhí)行
以前同步鎖的實(shí)現(xiàn)方式
在Objective-C中,屬性就是開(kāi)發(fā)者經(jīng)常需要同步的地方童太。通常開(kāi)發(fā)者想省事的話(huà)(以前我也是這樣覺(jué)得)米辐,會(huì)這樣寫(xiě):
- (NSString*)someString {@synchronized(self) {return_someString;? ? }}- (void)setSomeString:(NSString*)someString {@synchronized(self) {? ? ? ? _someString = someString;? ? }}
以上代碼除了上文提到的效率低以外胸完,還有一個(gè)問(wèn)題,就是該方法并不能保證訪(fǎng)問(wèn)該對(duì)象時(shí)絕對(duì)是線(xiàn)程安全的儡循。雖然舶吗,這種方法在訪(fǎng)問(wèn)屬性時(shí),確實(shí)是“原子”的择膝,也必定能從中獲取到有效值誓琼,然而在同一線(xiàn)程上多次調(diào)用getter方法,每次獲取到的結(jié)果未必相同肴捉。在兩次訪(fǎng)問(wèn)操作之間腹侣,其他線(xiàn)程可能會(huì)寫(xiě)入新的屬性值。此時(shí)齿穗,只能保證讀寫(xiě)操作是“原子”的傲隶,而多個(gè)線(xiàn)程的執(zhí)行順序,我們沒(méi)有辦法控制窃页。
使用GCD串行隊(duì)列來(lái)實(shí)現(xiàn)同步鎖
有種簡(jiǎn)單而高效的方法可以替代同步塊或鎖對(duì)象跺株,那就是使用“串行同步隊(duì)列”。將讀取操作以及寫(xiě)入操作都安排在同一個(gè)隊(duì)列里脖卖,即可保證數(shù)據(jù)同步乒省。
用法如下:
@property(nonatomic,strong)dispatch_queue_tsyncQueue;_syncQueue = dispatch_queue_create("com.effetiveobjectivec.syncQueue",NULL);- (NSString*)someString {? ? __blockNSString*localSomeString;dispatch_sync(_syncQueue, ^{? ? ? ? localSomeString = _someString;? ? });return_someString;}- (void)setSomeString:(NSString*)someString {dispatch_sync(_syncQueue, ^{? ? ? ? _someString = someString;? ? });}
此模式的思路是:把設(shè)置操作與獲取操作都安排在序列化的隊(duì)列里執(zhí)行,這樣的話(huà)畦木,所有針對(duì)屬性的訪(fǎng)問(wèn)操作就都同步了袖扛。
注:getter方法中,用一個(gè)臨時(shí)變量來(lái)保存值十籍,是因?yàn)樵赽lock中return的話(huà)蛆封,只是return到block中了,沒(méi)有真正返回到對(duì)應(yīng)的getter方法中勾栗,而__block是為了可以在block中改變改臨時(shí)變量而用惨篱。
雖然問(wèn)題解決了,但是我們還可以進(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)這種寫(xiě)法比原來(lái)慢她混。因?yàn)閳?zhí)行異步派發(fā)時(shí)烈钞,是需要拷貝塊泊碑。若拷貝塊所用的時(shí)間明顯超過(guò)執(zhí)行塊所需的時(shí)間,則這種做法將比原來(lái)的更慢毯欣。
注:本例子代碼比較簡(jiǎn)單馒过,若是要執(zhí)行的塊代碼邏輯比較復(fù)雜的話(huà),那么該寫(xiě)法可能還是比原來(lái)的塊些
使用GCD并發(fā)隊(duì)列來(lái)實(shí)現(xiàn)同步鎖
對(duì)于屬性的讀寫(xiě)酗钞,我們希望多個(gè)獲取方法可以并發(fā)執(zhí)行腹忽,而獲取方法與設(shè)置方法之間不能并發(fā)執(zhí)行,利用這個(gè)特點(diǎn)砚作,還能寫(xiě)出更快一些的代碼來(lái)窘奏。此時(shí)正可體現(xiàn)出GCD的好處。而用同步鎖或鎖對(duì)象葫录,是無(wú)法輕易實(shí)現(xiàn)下面這種方案的着裹。這次我們使用并發(fā)隊(duì)列:
@property(nonatomic,strong)dispatch_queue_tsyncQueue;_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);- (NSString*)someString {? ? __blockNSString*localSomeString;dispatch_sync(_syncQueue, ^{? ? ? ? localSomeString = _someString;? ? });returnlocalSomeString;}- (void)setSomeString:(NSString*)someString {dispatch_async(_syncQueue, ^{? ? ? ? _someString = someString;? ? });}
以上代碼,還無(wú)法正確實(shí)現(xiàn)同步米同。因?yàn)樗凶x寫(xiě)操作都會(huì)在同一個(gè)隊(duì)列上執(zhí)行骇扇,而該隊(duì)列是并發(fā)隊(duì)列,所有讀取和寫(xiě)入操作都可以隨時(shí)執(zhí)行面粮,沒(méi)有達(dá)到同步效果匠题。此問(wèn)題我們可以通過(guò)一個(gè)簡(jiǎn)單的GCD功能解決--柵欄(barrier)。下列函數(shù)可以向隊(duì)列中派發(fā)塊但金,將其作為柵欄使用:
voiddispatch_barrier_async(dispatch_queue_tqueue,dispatch_block_tblock);voiddispatch_barrier_sync(dispatch_queue_tqueue,dispatch_block_tblock);
注:dispatch_barrier_async如果傳入自己創(chuàng)建的并行隊(duì)列時(shí),會(huì)阻塞當(dāng)前隊(duì)列執(zhí)行郁季,而不阻塞當(dāng)前線(xiàn)程冷溃。
dispatch_barrier_sync如果傳入自己創(chuàng)建的并行隊(duì)列時(shí),阻塞當(dāng)前隊(duì)列的同時(shí)也會(huì)阻塞當(dāng)前線(xiàn)程梦裂,請(qǐng)注意
并發(fā)隊(duì)列如果發(fā)現(xiàn)接下來(lái)要處理的塊是個(gè)柵欄塊似枕,那么就一直要等當(dāng)前所有并發(fā)塊都執(zhí)行完畢,才會(huì)單獨(dú)執(zhí)行這個(gè)柵欄塊年柠。這待柵欄塊執(zhí)行完畢凿歼,在按正常方式繼續(xù)向下處理。這樣就解決了并發(fā)隊(duì)列的同步問(wèn)題冗恨。
GCD并發(fā)隊(duì)列中加入柵欄
本例中答憔,可以用柵欄塊來(lái)實(shí)現(xiàn)屬性的設(shè)置方法。在設(shè)置方法中使用了柵欄塊之后掀抹,對(duì)屬性的讀取操作依然可以并發(fā)執(zhí)行虐拓,但寫(xiě)入操作卻必須單獨(dú)執(zhí)行了。在下圖中演示的這個(gè)隊(duì)列中傲武,有多個(gè)讀取操作蓉驹,而且還有一個(gè)寫(xiě)入操作城榛。
在這個(gè)并發(fā)隊(duì)列中,讀取操作是用普通的塊來(lái)實(shí)現(xiàn)的态兴,而寫(xiě)入操作則是用柵欄塊來(lái)實(shí)現(xiàn)的
讀取操作可以并行狠持,但寫(xiě)入操作必須單獨(dú)執(zhí)行,因?yàn)樗菛艡趬K
實(shí)現(xiàn)代碼很簡(jiǎn)單:
@property(nonatomic,strong)dispatch_queue_tsyncQueue;//_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);/* 這里應(yīng)該使用自己創(chuàng)建的并發(fā)隊(duì)列瞻润,因?yàn)樘O(píng)果文檔中指出喘垂,如果使用的是全局隊(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 {? ? __blockNSString*localSomeString;dispatch_sync(_syncQueue, ^{? ? ? ? localSomeString = _someString;? ? });returnlocalSomeString;}- (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í)間了