同步鎖解決方案

前言:

??在Objective-C中,如果有多個(gè)線程要執(zhí)行同一份代碼,那么有時(shí)候會(huì)出問題。這種情況下,通常使用鎖來實(shí)現(xiàn)某種同步機(jī)制。在GCD出現(xiàn)之前,有兩種方法,第一種采用內(nèi)置的"同步塊"(synchronization block):

-(void)synchronizedMethod{
@synchronized(self){
//Safe
}
}
??這種寫法會(huì)根據(jù)給定的對(duì)象,自動(dòng)創(chuàng)建一個(gè)鎖,并等待塊中的代碼執(zhí)行完畢。執(zhí)行到這段代碼結(jié)尾處,鎖就釋放了。在本例中,同步行為所針對(duì)的對(duì)象是self。這么寫通常沒錯(cuò),因?yàn)樗梢员WC每個(gè)對(duì)象實(shí)例都能不受干擾地運(yùn)行其synchronizedMethod方法。然而,濫用@synchronized(self)則會(huì)降低代碼效率,因?yàn)楣灿猛粋€(gè)鎖的那些同步塊,都必須按順序執(zhí)行。若是在self對(duì)象上頻繁加鎖,那么程序可能等另一段與此無關(guān)的代碼執(zhí)行完畢,才能繼續(xù)執(zhí)行當(dāng)前代碼,這樣做其實(shí)并沒有必要。

另一個(gè)辦法是直接使用NSLock對(duì)象:

_lock = [[NSLock alloc] init];

-(void)synchronizedMethod{
[_lock lock];
//Safe
[_lock unlock];
}

??也可以使用NSRecursiveLock 這種遞歸鎖(recursive lock),線程能夠多次持有該鎖,而不會(huì)出現(xiàn)死鎖(deadlock)現(xiàn)象。
??這兩種方法都很好,不過也有其缺陷。比方說,在極端情況下,同步塊會(huì)導(dǎo)致死鎖,另外,其效率也不見得很高,而如果直接使用鎖對(duì)象的話,一旦遇到死鎖,就會(huì)非常麻煩。
??替代方案就是使用GCD,它能以更簡單、更高效的形式為代碼加鎖。比方說,屬性就是開發(fā)者經(jīng)常需要同步的地方,這種屬性需要做成原子的。用atomic特性來修飾屬性,即可實(shí)現(xiàn)這一點(diǎn)。而開發(fā)者如果想自己編寫訪問方法的話,那么通常會(huì)這樣寫:

-(void)someString{
@synchronized(self){
return _someString;
}
}

-(void)setSomeString:(NSString *)someString{
@synchronized(self){
_someString = someString;
}
}

??剛才說過,濫用@synchronized(self)會(huì)很危險(xiǎn),因?yàn)樗型綁K都會(huì)彼此搶奪同一個(gè)鎖。要是有很多屬性都這么寫的話,那么每個(gè)屬性的同步塊都要等其他所有同步塊執(zhí)行完畢才能執(zhí)行,這也許并不是開發(fā)者想要的效果。我們只是想令每個(gè)屬性各自獨(dú)立地同步。
??順便說一下,這么做雖然能提供某種程度的線程安全,但卻無法保證訪問該對(duì)象時(shí)該對(duì)象絕對(duì)是線程安全的。當(dāng)然,訪問屬性的操作確實(shí)是原子的。使用屬性時(shí),必定能從中獲取到有效值,然而在同一個(gè)線程上多次調(diào)用獲取方法(getter),每次獲取到的結(jié)果卻未必相同。在兩次訪問操作之間,其他線程可能會(huì)寫入新的屬性值。
??有種簡單而高效的方法可以替代同步塊或鎖對(duì)象,那就是使用"串行同步隊(duì)列"(serial synchronization queue)。將讀取操作以及寫入操作都安排在同一個(gè)隊(duì)列中,即可保證數(shù)據(jù)同步。
其用法如下:

_syncQueue = dispatch_queue_create("com.effective.syncQueue",NULL);
-(NSString *)someString{
__block NSString *localSomeString;
dispatch_sync(_syncQueue,^{
localSomeString = _someString;
});
}
-(void)setSomeString:(NSString *)someString{
dispatch_sync(_syncQueue,^{
_someString = someString;
});
}
??此模式的思路是:把設(shè)置操作與獲取操作都安排在序列化隊(duì)列里執(zhí)行,這樣的話,所有針對(duì)屬性的訪問操作就都同步了。為了使塊代碼能夠設(shè)置局部變量,獲取方法中用到了__block語法,若是拋開這一點(diǎn),那么這種寫法要比前面那些更為簡潔。全部加鎖任務(wù)都在GCD中處理,而GCD是在相當(dāng)深的底層來實(shí)現(xiàn)的,于是能夠做許多優(yōu)化。因此,開發(fā)者無須擔(dān)心那些事,只要專心把訪問方法寫好就行。
??然而還可以進(jìn)一步優(yōu)化。設(shè)置方法并不一定非得是同步的。設(shè)置實(shí)例變量所用的塊,并不需要想設(shè)置方法返回什么值。也就是說,設(shè)置方法的代碼可以改成下面這樣:

-(void)setSomeString:(NSString *)someString{
dispatch_async(_syncQueue,^{
_someString = someString;
});
}

??這次只是把同步派發(fā)改成了異步派發(fā),從調(diào)用者的角度來看,這個(gè)小改動(dòng)可以提升設(shè)置方法的執(zhí)行速度,而讀取操作與寫入操作依然會(huì)按順序執(zhí)行。但這么改有個(gè)壞處:如果你測(cè)一下程序性能,那么可能會(huì)發(fā)現(xiàn)這種寫法比原來慢,因?yàn)閳?zhí)行異步派發(fā)時(shí),需要拷貝塊。若拷貝塊所用的事件明顯超過執(zhí)行塊所花的事件,則這種做法將比原來更慢。由于本書所舉的這個(gè)例子很簡單,所以改完之后很可能會(huì)變慢。然而,若是派發(fā)給隊(duì)列的塊要執(zhí)行更為繁重的任務(wù),那么仍然可以考慮這種備選方案。
??多個(gè)獲取方法可以并發(fā)執(zhí)行,而獲取方法與設(shè)置方法之間不能并發(fā)執(zhí)行,利用這個(gè)特點(diǎn),還能寫出更快一些的代碼來。此時(shí)正可以提現(xiàn)GCD寫法的好處來。用同步塊或鎖對(duì)象,是無法輕易實(shí)現(xiàn)下面這種方案的。這次不用串行隊(duì)列,而改用并發(fā)隊(duì)列。(concurrent queue)

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
-(NSString *)someString{
__block NSString *localSomeString;
dispatch_sync(_syncQueue,^{
localSomeString = _someString;
});
}
-(void)setSomeString:(NSString *)someString{
dispatch_async(_syncQueue,^{
_someString = someString;
});
}

??像現(xiàn)在這樣寫代碼,還無法正確實(shí)現(xiàn)同步。所有讀操作與寫入操作都會(huì)在同一個(gè)隊(duì)列中執(zhí)行,不過由于是并發(fā)隊(duì)列,所以讀取與寫入操作可以隨時(shí)執(zhí)行。而我們恰恰不想讓這些操作隨意執(zhí)行。此問題用一個(gè)簡單的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);

??在隊(duì)列中,柵欄塊必須單獨(dú)執(zhí)行,不能與其他塊并行。這只對(duì)并發(fā)隊(duì)列有意義,因?yàn)榇嘘?duì)列中的塊總是按順序逐個(gè)執(zhí)行的。并發(fā)隊(duì)列如果發(fā)現(xiàn)接下來要處理的塊是個(gè)柵欄塊(barrier block),那么就一直要等待當(dāng)前所有并發(fā)塊都執(zhí)行完畢,才會(huì)單獨(dú)執(zhí)行這個(gè)柵欄塊。待柵欄塊執(zhí)行過后,再按正常方法繼續(xù)向下處理。

??在本例中,可以用柵欄塊來實(shí)現(xiàn)屬性的設(shè)置方法。在設(shè)置方法中使用了柵欄塊之后,對(duì)屬性的讀取操作依然可以并發(fā)執(zhí)行,但是寫入操作卻必須單獨(dú)執(zhí)行了。實(shí)現(xiàn)代碼如下:

_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_barrier_async(_syncQueue,^{
_someString = someString;
});
}


在這個(gè)并發(fā)隊(duì)列中,讀取操作是用普通的塊來實(shí)現(xiàn)的,而寫入操作則是用柵欄塊來實(shí)現(xiàn)的。讀取操作可以并行,但寫入操作必須單獨(dú)執(zhí)行,因?yàn)樗菛艡趬K

??測(cè)試一下性能,你就會(huì)發(fā)現(xiàn),這種做法肯定比使用串行隊(duì)列要快。注意設(shè)置函數(shù)也可以改用同步的柵欄塊(synchronous barrier)來實(shí)現(xiàn),那樣做可能會(huì)更高效,原因執(zhí)行異步派發(fā),需要拷貝塊。最好還是測(cè)一測(cè)每種做法的性能,然后從中選出最適合當(dāng)前場(chǎng)景的方案。

總結(jié):

*派發(fā)隊(duì)列可用來表示同步語義(synchronization semantic),這種做法要比使用@synchronized塊或NSLock對(duì)象更簡單

*將同步與異步派發(fā)結(jié)合起來,可以實(shí)現(xiàn)與普通加鎖機(jī)制一樣的同步行為,而這么做卻不會(huì)阻塞執(zhí)行異步派發(fā)的線程。

*使用同步隊(duì)列及柵欄塊,可以令同步塊行為更為高效??傊?,多用派發(fā)隊(duì)列,少用同步鎖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容