在iOS開發(fā)中會遇到多個線程要執(zhí)行同一份代碼的情況,比如同時修改統(tǒng)一個數(shù)據(火車賣票),通俗一點說就是搶占資源的問題。
通常要使用加鎖來實現(xiàn)這種同步機制,在GCD出現(xiàn)之前,一共有兩種方法。
1、同步塊(synchroization block)
-(void)sunchronizedMethod{
@synchronized(self) {
//安全代碼
}
}
這樣寫會自動創(chuàng)建一個鎖,等塊中代碼都執(zhí)行完畢,執(zhí)行到這段代碼結尾,鎖就釋放了。這樣寫通常沒錯,因為它可以保證每個self持有的對象都能不受干擾的運行其sunchronizedMethod方法,然而,濫用@synchronized(self)會降低代碼效率,因為共用同一個鎖的那些同步塊(synchroization block),都必須按順序執(zhí)行,若是在self對象上頻繁加鎖,那么程序可能要等另一段與此無關的代碼執(zhí)行完畢,才能繼續(xù)執(zhí)行當前代碼,其實沒有這樣做的必要。
2、NSLock
_lock = [NSLock alloc]init];
-(void)sunchronizedMethod{
[_lock lock];
//安全代碼
[_lock unlock];
}
這兩種方法都很好,不過也有缺陷,比方說,在極端的情況下,同步塊(synchroization block)會導致死鎖,另外,其效率也不見得很高,而如果用NSLock的話,一旦遇到死鎖,就會非常麻煩。
最好的方案就是使用GCD,它能簡單高效的方式給代碼加鎖。下面通過get和set方法來舉例說明:當我們操作一個對象的時候,假設將屬性設為atomic,那么實現(xiàn)的效果類似于以下代碼:
-(NSString *)someString{
@synchronized(self){
return _someString;
}
}
-(void)setSomeString:(NSString *)someString{
@synchronized(self){
_someString = someString;
}
}
剛才說過濫用@synchronized(self)很危險,因為所有同步塊(synchroization block)都會彼此搶奪同一個鎖。要是有很多屬性都這么寫的話,那么每個屬性的同步塊(synchroization block)都要等其他所有同步塊(synchroization block)執(zhí)行完畢才能執(zhí)行,這也許并不是開發(fā)者想要的效果。我們只想令每個屬性各自獨立的同步。這么做雖然能夠提供某種程度的線程安全,但卻無法保證訪問該對象時絕對是線程安全的。當然,當訪問屬性的操作是確實是原子性的(atomic)。使用屬性時,必定能從中獲取到有效值,然而在同一線程上多次調用get方法,每次的結果卻未必相同,因為在兩次get方法之間,其他線程可能會將新的值寫入屬性。
有一種簡單高效的方法可以代替同步塊(synchroization block)和NSLock對象,那就是使用“串行同步隊列”。將讀取和寫入操作都安排到同一個隊列中,即可保證數(shù)據同步。用法如下:
//NULL相當于DISPATCH_QUEUE_SERIAL
_syncQueue = dispatch_queue_create("www.nnn.com", NULL);
-(NSString *)someString{
__block NSString *str;
dispatch_sync(_syncQueue, ^{
str = _someString;
});
return str;
}
-(void)setSomeString:(NSString *)someString{
dispatch_sync(_syncQueue, ^{
_someString = someString;
});
}
這種實現(xiàn)的思路是:把set操作與get操作都安排到序列化的隊列里執(zhí)行,這樣的話,所有針對屬性的訪問操作就同步了。全部加鎖任務都在GCD重處理,而GCD是在相當深的底層來實現(xiàn)的,于是能夠做許多優(yōu)化,因此開發(fā)者無須擔心那些事,只要專心把訪問方法寫好就行。
然而還可以進一步優(yōu)化,set方法不一定非得是同步的。設置實例變量所用的方法,并不需要返回什么值,也就是實說,可以改成下面那樣:
-(void)setSomeString:(NSString *)someString{
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
這次只是把同步派發(fā)改成了異步派發(fā),從調用者的角度來看,這個小改動可以提升set方法的執(zhí)行速度,而get方法依然會按照順序執(zhí)行。但這么做有個壞處,有可能程序會比較慢,因為執(zhí)行異步派發(fā)的時候,需要拷貝塊,若拷貝塊所用的時間明顯超過執(zhí)行塊所花的時間,則這種做法比原來更慢。然而,若是派發(fā)給隊列的塊需要執(zhí)行更為繁重的任務,那么仍然可以考慮這種方案。
多個獲取方法可以并發(fā)執(zhí)行,而set方法和get方法之間不能并發(fā)執(zhí)行,利用這個特點,還能寫出更快的代碼來,我們這次改用并發(fā)隊列:
_syncQueue = dispatch_queue_create("www.nnn.com", DISPATCH_QUEUE_CONCURRENT);
-(NSString *)someString{
__block NSString *str;
dispatch_sync(_syncQueue, ^{
str = _someString;
});
return str;
}
-(void)setSomeString:(NSString *)someString{
dispatch_sync(_syncQueue, ^{
_someString = someString;
});
}
像這樣寫代碼,還無法實現(xiàn)同步。所有get操作和set操作都會在同一個隊列上執(zhí)行,不過由于是并發(fā)隊列,所以set和get操作可以隨時執(zhí)行,而這是我們恰恰不想看到的。此問題用一個簡單的GCD功能就能解決,他就是柵欄(barrier)。下面的函數(shù)可以向隊列中派發(fā)塊,將其作為柵欄(barrier)使用:
void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
在隊列中,柵欄塊必須單獨執(zhí)行,不能與其他塊并行。這只對并發(fā)隊列有意義,因為串行本來就是順序逐個執(zhí)行的。并發(fā)隊列如果發(fā)現(xiàn)接下來要處理的塊是個柵欄塊(barrier block),那么久一直要等到當前所有并發(fā)塊都執(zhí)行完畢,才會單獨執(zhí)行這個柵欄塊,待柵欄塊執(zhí)行過后,再按正常方式繼續(xù)鄉(xiāng)下處理。這也就是說,當我們進行set的時候,這個塊是單獨執(zhí)行的,這樣就不會出現(xiàn)多個線程同時寫入數(shù)據的情況。
代碼如下:
_syncQueue = dispatch_queue_create("www.nnn.com", DISPATCH_QUEUE_CONCURRENT);
-(NSString *)someString{
__block NSString *str;
dispatch_sync(_syncQueue, ^{
str = _someString;
});
return str;
}
-(void)setSomeString:(NSString *)someString{
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}