通常狀況下,iOS開發者只要會使用GCD、@synchronized、NSLock等幾個簡單的API,就能夠應對大部分多線程開發了,不過這樣是否真正作到了多線程安全,又是否真正充分利用了多線程的效率優點呢?看看如下幾個容易被忽略的細節。node
先看下讀者寫者問題的描述:面試
有讀者和寫者兩組併發線程,共享同一數據,當兩個或以上的讀線程同時訪問共享數據時不會產生反作用,但若某個寫線程和其餘線程(讀線程或寫線程)同時訪問共享數據時則可能致使數據不一致的錯誤。所以要求:算法
容許多個讀者能夠同時對共享數據執行讀操做;swift
只容許一個寫者寫共享數據;安全
任一寫者在完成寫操做以前不容許其餘讀者或寫者工做;bash
寫者執行寫操做前,應讓已有的讀者和寫者所有退出。網絡
從以上描述能夠得知,所謂「讀者寫者問題」是指保證一個寫線程必須與其餘線程互斥地訪問共享對象的同步問題,容許併發讀操做,可是寫操做必須和其餘讀寫操做是互斥的。數據結構
大部分客戶端App作的事情無非就是從網絡拉取最新數據、加工數據、展示列表,這個過程當中既有拿到最新數據後寫入本地的操做,也有上層業務對本地數據的讀取操做,所以會牽涉大量的多線程讀寫操做,很顯然,這些基本都屬於讀者寫者問題的範疇[1]。多線程
然而筆者注意到,在遇到多線程讀寫問題時,多數iOS開發者都會當即想到加鎖,或者乾脆避免使用多線程,但卻少有人會嘗試用讀者寫者問題的思路去進一步提高效率。併發
給你們推薦一個iOS技術交流羣!763164022羣內提供數據結構與算法、底層進階、swift、逆向、底層面試題整合文檔等免費資料!
如下是實現一個簡單cache的示例代碼:
//實現一個簡單的cache
- (void)setCache:(id)cacheObject forKey:(NSString *)key {
if (key.length == 0) {
return;
}
[_cacheLock lock];
self.cacheDic[key] = cacheObject;
...
[_cacheLock unlock];
}
- (id)cacheForKey:(NSString *key) {
if (key.length == 0) {
return nil;
}
[_cacheLock lock];
id cacheObject = self.cacheDic[key];
...
[_cacheLock unlock];
return cacheObject;
}
複製代碼
上述代碼用互斥鎖來實現多線程讀寫,作到了數據的安全讀寫,可是效率卻並非最高的,由於這種狀況下,雖然寫操做和其餘操做之間是互斥的,但同時讀操做之間卻也是互斥的,這會浪費cpu資源,如何改良呢?不難發現,這實際上是個典型的讀者寫者問題。先看下解決讀者寫者問題的僞代碼:
semaphore ReaderWriterMutex = 1; //實現讀寫互斥
int Rcount = 0; //讀者數量
semaphore CountMutex = 1; //讀者修改計數互斥
writer(){
while(true){
P(ReaderWriterMutex);
write;
V(ReaderWriterMutex);
}
}
reader(){
while(true){
P(CountMutex);
if(Rcount == 0) //當第一個讀者進來時,阻塞寫者
P(ReaderWriterMutex);
++Rcount;
V(CountMutex);
read;
P(CountMutex);
--Rcount;
if(Rcount == 0)
V(ReaderWriterMutex); //當最後一個讀者離開後,釋放寫者
V(CountMutex);
}
}
複製代碼
在iOS中,上述代碼中的PV原語能夠替換成GCD中的信號量API,dispatch_semaphore_t來實現,可是須要額外維護一個readerCount以及實現readerCount互斥訪問的信號量,手動實現比較麻煩,封裝成統一接口有必定難度。不過好在iOS開發中能夠找到現成的讀者寫者鎖:
這是一個古老的C語言層面的函數,用法以下:
// Initialization of lock, pthread_rwlock_t is a value type and must be declared as var in order to refer it later. Make sure not to copy it.
var lock = pthread_rwlock_t()
pthread_rwlock_init(&lock, nil)
// Protecting read section:
pthread_rwlock_rdlock(&lock)
// Read shared resource
pthread_rwlock_unlock(&lock)
// Protecting write section:
pthread_rwlock_wrlock(&lock)
// Write shared resource
pthread_rwlock_unlock(&lock)
// Clean up
pthread_rwlock_destroy(&lock)
複製代碼
接口簡潔可是卻不友好,須要注意pthread_rwlock_t是值類型,用=賦值會直接拷貝,不當心就會浪費內存,另外用完後還須要記得銷燬,容易出錯,有沒有更高級更易用的API呢?
dispatch_barrier_async / dispatch_barrier_sync並非專門用來解決讀者寫者問題的,barrier主要用於如下場景:當執行某一任務A時,須要該隊列上以前添加的全部操做都執行完,而以後添加進來的任務,須要等待任務A執行完畢才能夠執行,從而達到將任務A隔離的目的,具體過程以下圖所示:
若是將barrier任務以前和以後的併發任務換爲讀操做,barrier任務自己換爲寫操做,就能夠將dispatch_barrier_async / dispatch_barrier_sync當作讀者寫者鎖來使用了,下面把文初的使用普通鎖實現的cache代碼,用dispatch_barrier_async重寫,作下對比:
//實現一個簡單的cache(使用普通鎖)
- (void)setCache:(id)cacheObject forKey:(NSString *)key {
if (key.length == 0) {
return;
}
[_cacheLock lock];
self.cacheDic[key] = cacheObject;
...
[_cacheLock unlock];
}
- (id)cacheForKey:(NSString *key) {
if (key.length == 0) {
return nil;
}
[_cacheLock lock];
id cacheObject = self.cacheDic[key];
...
[_cacheLock unlock];
return cacheObject;
}
複製代碼
//實現一個簡單的cache(使用讀者寫者鎖)
static dispatch_queue_t queue = dispatch_queue_create("com.gfzq.testQueue", DISPATCH_QUEUE_CONCURRENT);
- (void)setCache:(id)cacheObject forKey:(NSString *)key {
if (key.length == 0) {
return;
}
dispatch_barrier_async(queue, ^{
self.cacheDic[key] = cacheObject;
...
});
}
- (id)cacheForKey:(NSString *key) {
if (key.length == 0) {
return nil;
}
__block id cacheObject = nil;
dispatch_sync(queue, ^{
cacheObject = self.cacheDic[key];
...
});
return cacheObject;
}
複製代碼
這樣實現的cache就能夠併發執行讀操做,同時又有效地隔離了寫操做,兼顧了安全和效率。
對於聲明爲atomic並且又本身手動實現getter或者setter的屬性,也能夠用barrier來改進:
@property (atomic, copy) NSString *someString;
- (NSString *)someString {
__block NSString *tempString;
dispatch_sync(_syncQueue, ^{
tempString = _someString;
});
return tempString;
}
- (void)setSomeString :(NSString *)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString
...
}
}
複製代碼
在作到atomic的同時,getter之間還能夠併發執行,比直接把setter和getter都放到串行隊列或者加普通鎖要更高效。
使用讀者寫者鎖必定比全部讀寫都加鎖以及使用串行隊列要快,可是到底能快多少呢?Dmytro Anokhin在[3]中作了實驗對比,測出了分別使用NSLock、GCD barrier和pthread_rwlock時獲取鎖所須要的平均時間,實驗樣本數在100到1000之間,去掉最高和最低的10%,結果以下列圖表所示:
分析可知:
(1)使用讀者寫者鎖(GCD barrier、pthread_rwlock),相比單純使用普通鎖(NSLock),效率有顯著提高;
(2)讀者數量越多,寫者數量越少,使用讀者寫者鎖的效率優點越明顯;
(3)使用GCD barrier和使用pthread_rwlock的效率差別不大。
因爲pthread_rwlock不易使用且容易出錯,並且GCD barrier和pthread_rwlock對比性能至關,建議使用GCD barrier來解決iOS開發中遇到的讀者寫者問題。另外,使用GCD還有個潛在優點:GCD面向隊列而非線程,dispatch至某一隊列的任務,可能在任一線程上執行,這些對開發者是透明的,這樣設計的好處顯而易見,GCD能夠根據實際狀況從本身管理的線程池中挑選出開銷最小的線程來執行任務,最大程度減少context切換次數。
須要注意的是,並不是全部的多線程讀寫場景都必定是讀者寫者問題,使用時要注意辨別。例如如下YYCache的代碼:
//讀cache
- (id)objectForKey:(id)key {
if (!key) return nil;
pthread_mutex_lock(&_lock);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
if (node) {
node->_time = CACurrentMediaTime();
[_lru bringNodeToHead:node];
}
pthread_mutex_unlock(&_lock);
return node ? node->_value : nil;
}
複製代碼
//寫cache
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
if (!key) return;
if (!object) {
[self removeObjectForKey:key];
return;
}
pthread_mutex_lock(&_lock);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
NSTimeInterval now = CACurrentMediaTime();
if (node) {
_lru->_totalCost -= node->_cost;
_lru->_totalCost += cost;
node->_cost = cost;
node->_time = now;
node->_value = object;
[_lru bringNodeToHead:node];
} else {
node = [_YYLinkedMapNode new];
node->_cost = cost;
node->_time = now;
node->_key = key;
node->_value = object;
[_lru insertNodeAtHead:node];
}
if (_lru->_totalCost > _costLimit) {
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}
if (_lru->_totalCount > _countLimit) {
_YYLinkedMapNode *node = [_lru removeTailNode];
if (_lru->_releaseAsynchronously) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[node class]; //hold and release in queue
});
} else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
[node class]; //hold and release in queue
});
}
}
pthread_mutex_unlock(&_lock);
}
複製代碼
這裏的cache因爲使用了LRU淘汰策略,每次在讀cache的同時,會將本次的cache放到數據結構的最前面,從而延緩最近使用的cache被淘汰的時機,由於每次讀操做的同時也會發生寫操做,因此這裏直接使用pthread_mutex互斥鎖,而沒有使用讀者寫者鎖。
綜上所述,若是你所遇到的多線程讀寫場景符合: (1)存在單純的讀操做(即讀任務裏沒有同時包含寫操做); (2)讀者數量較多,而寫者數量較少。 都應該考慮使用讀者寫者鎖來進一步提高併發率。
注意: (1)讀者寫者問題包含「讀者優先」和「寫者優先」兩類:前者表示讀線程只要看到有其餘讀線程正在訪問文件,就能夠繼續做讀訪問,寫線程必須等待全部讀線程都不訪問時才能寫文件,即便寫線程可能比一些讀線程更早提出申請;而寫者優先表示寫線程只要提出申請,再後來的讀線程就必須等待該寫線程完成。GCD的barrier屬於寫者優先的實現。具體請參考文檔[2]。 (2)串行隊列上不必使用GCD barrier,應該使用dispatch_queue_create創建的併發隊列;dispatch_get_global_queue因爲是全局共享隊列,使用barrier達不到隔離當前任務的效果,會自動降級爲dispatch_sync / dispatch_async。[
首先看兩段代碼: 代碼段1
@property (atomic, copy) NSString *atomicStr;
//thread A
atomicSr = @"am on thread A";
NSLog(@"%@", atomicStr);
//thread B
atomicSr = @"am on thread B";
NSLog(@"%@", atomicStr);
複製代碼
代碼段2
- (void)synchronizedAMethod {
@synchronized (self) {
...
}
}
- (void)synchronizedBMethod {
@synchronized (self) {
...
}
}
- (void)synchronizedCMethod {
@synchronized (self) {
...
}
}
複製代碼
執行代碼段1,在線程A上打印出來的字符串卻多是「am on thread B」,緣由是雖然atomicStr是原子操做,可是取出atomicStr以後,在執行NSLog以前,atomicStr仍然可能會被線程B修改。所以atomic聲明的屬性,只能保證屬性的get和set是完整的,可是卻不能保證get和set完以後的關於該屬性的操做是多線程安全的,這就是aomic聲明的屬性不必定能保證多線程安全的緣由。
一樣的,不只僅是atomic聲明的屬性,在開發中本身加的鎖若是粒度過小,也不能保證線程安全,代碼段1其實和下面代碼效果一致:
@property (nonatomic, strong) NSLock *lock;
@property (nonatomic, copy) NSString *atomicStr;
//thread A
[_lock lock];
atomicSr = @"am on thread A";
[_lock unlock];
NSLog(@"%@", atomicStr);
//thread B
[_lock lock];
atomicSr = @"am on thread B";
[_lock unlock];
NSLog(@"%@", atomicStr);
複製代碼
若是想讓程序按照咱們的初衷,設置完atomicStr後打印出來的就是設置的值,就須要加大鎖的範圍,將NSLog也包括在臨界區內:
//thread A
[_lock lock];
atomicSr = @"am on thread A";
NSLog(@"%@", atomicStr);
[_lock unlock];
//thread B
[_lock lock];
atomicSr = @"am on thread B";
NSLog(@"%@", atomicStr);
[_lock unlock];
複製代碼
示例代碼很簡單,很容易看出問題所在,可是在實際開發中遇到更復雜些的代碼塊時,一不當心就可能踏入坑裏。所以在設計多線程代碼時,要特別注意代碼之間的邏輯關係,若後續代碼依賴於加鎖部分的代碼,那這些後續代碼也應該一併加入鎖中。
@synchronized關鍵字會自動根據傳入對象建立一個與之關聯的鎖,在代碼塊開始時自動加鎖,並在代碼塊結束後自動解鎖,語法簡單明瞭,很方便使用,可是這也致使部分開發者過渡依賴於@synchronized關鍵字,濫用@synchronized(self)。如上述代碼段2中的寫法,在一整個類文件裏,全部加鎖的地方用的都是@synchronized(self),這就可能會致使不相關的線程執行時都要互相等待,本來能夠併發執行的任務不得不串行執行。另外使用@synchronized(self)還可能致使死鎖:
//class A
@synchronized (self) {
[_sharedLock lock];
NSLog(@"code in class A");
[_sharedLock unlock];
}
//class B
[_sharedLock lock];
@synchronized (objectA) {
NSLog(@"code in class B");
}
[_sharedLock unlock];
複製代碼
緣由是由於self極可能會被外部對象訪問,被用做key來生成一鎖,相似上述代碼中的@synchronized (objectA)。兩個公共鎖交替使用的場景就容易出現死鎖。因此正確的作法是傳入一個類內部維護的NSObject對象,並且這個對象是對外不可見的[2]。
所以,不相關的多線程代碼,要設置不一樣的鎖,一個鎖只管一個臨界區。除此以外,還有種常見的錯誤作法會致使併發效率降低:
//thread A
[_lock lock];
atomicSr = @"am on thread A";
NSLog(@"%@", atomicStr);
//do some other tasks which are none of business with atomicStr;
for (int i = 0; i < 100000; i ++) {
sleep(5);
}
[_lock unlock];
//thread B
[_lock lock];
atomicSr = @"am on thread B";
NSLog(@"%@", atomicStr);
//do some other tasks which are none of business with atomicStr;
for (int i = 0; i < 100000; i ++) {
sleep(5);
}
[_lock unlock];
複製代碼
即在臨界區內包含了與當前加鎖對象無關的任務,實際應用中,須要咱們尤爲注意臨界區內的每個函數,由於其內部實現可能調用了耗時且無關的任務。
相比較上述提到的@synchronized(self),下面這種情形引發的死鎖更加常見:
@property (nonatomic,strong) NSLock *lock;
_lock = [[NSLock alloc] init];
- (void)synchronizedAMethod {
[_lock lock];
//do some tasks
[self synchronizedBMethod];
[_lock unlock];
}
- (void)synchronizedBMethod {
[_lock lock];
//do some tasks
[_lock unlock];
}
複製代碼
A方法已獲取鎖後,再調用B方法,就會觸發死鎖,B方法在等待A方法執行完成釋放鎖後才能繼續執行,而A方法執行完成的前提是執行完B方法。實際開發中,可能發生死鎖的情形每每隱蔽在方法的層層調用中。所以建議在不能肯定是否會產生死鎖時,最好使用遞歸鎖。更保守一點的作法是不論什麼時候都使用遞歸鎖,由於很難保證之後的代碼會不會在同一線程上屢次加鎖。
遞歸鎖容許同一個線程在未釋放其擁有的鎖時反覆對該鎖進行加鎖操做,內部經過一個計數器來實現。除了NSRecursiveLock,也可使用性能更佳的pthread_mutex_lock,初始化時參數設置爲PTHREAD_MUTEX_RECURSIVE便可:
pthread_mutexattr_t attr;
pthread_mutexattr_init (&attr);
pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init (&_lock, &attr);
pthread_mutexattr_destroy (&attr);
複製代碼
值得注意的是,@synchronized內部使用的也是遞歸鎖:
// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}
複製代碼
想寫出高效、安全的多線程代碼,只是熟悉GCD、@synchronized、NSLock這幾個API是不夠的,還須要瞭解更多API背後的知識,深入理解臨界區的概念、理清各個任務之間的時序關係是必要條件。
給你們推薦一個iOS技術交流羣!763164022羣內提供數據結構與算法、底層進階、swift、逆向、底層面試題整合文檔等免費資料!
做者:Eternal_Love 連接:www.jianshu.com/p/b053b3c3c… 轉載簡書:簡書