iOS常用鎖的研究
背景
iOS并發(fā)編程除了常用的多線程技術(shù)外,線程間同步的方法也是另外一個重要的點(diǎn). 公司的項(xiàng)目時間跨度比較長,多線程的實(shí)現(xiàn)方式從早期的NSThread到現(xiàn)在GCD和NSOperation+NSOperationQueue的方式都有,加鎖方式也是多種多樣.之前并不清楚各種鎖的使用場景和效率,這里做了簡單的研究把總結(jié)的一點(diǎn)心得記錄下來.
OSSpinLock
OSSpinLock為什么第一個提起呢?因?yàn)槠湫适亲罡叩?本人是較晚才接觸到這個鎖的,最早是從facebook的KVOController里面看到的(不過現(xiàn)在已經(jīng)用pthread_mutex重寫,原因下面會提到).
自旋鎖不會引起調(diào)用者睡眠,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖,"自旋"一詞就是因此而得名.
因?yàn)樯倭藸顟B(tài)切換,所以效率也會高一些,但是如果執(zhí)行的代碼本身比較耗時且調(diào)用的頻次也比較頻繁的話,那么CPU的壓力會陡增且效率也會大打折扣.
所以一般的鎖住部分的代碼段執(zhí)行的操作比較輕量級那么OSSpinLock還是非常高效的,比如訪問一些可變集合類型的變量.
下面是維基百科關(guān)于SpinLock的說明,同樣適用于OSSpinLock.
后來偶然讀到一篇博客<不再安全的 OSSpinLock>,iOS中使用OSSpinLock已經(jīng)不再安全了,包括Apple自己也對代碼中的OSSpinLock做了替換,Google protobuf的objective-c的源碼中對OSSpinLock也用dispatch_semaphore進(jìn)行了替換,另外一種替換方式是采用pthread_mutex如上面提到的KVOController.
示例代碼:
[OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
// do something
OSSpinLockUnlock(&lock);
pthread_mutex
互斥鎖,這個是C語言形式的加鎖方式,最早是在<Unix高級編程>里面接觸到的.
互斥鎖的使用過程中,主要有pthread_mutex_init, pthread_mutex_destory, pthread_mutex_lock, pthread_mutex_unlock這幾個函數(shù)以完成鎖的初始化,鎖的銷毀,上鎖和釋放鎖操作.
示例代碼:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
// do something
pthread_mutex_unlock(&mutex);
小結(jié)1
Mutex和SpinLock的區(qū)別(以下是網(wǎng)上一篇博客的摘錄,個人認(rèn)為解釋的比較好):
實(shí)現(xiàn)原理上來講,Mutex屬于sleep-waiting類型的鎖。例如在一個雙核的機(jī)器上有兩個線程(線程A和線程B),它們分別運(yùn)行在Core0和 Core1上。假設(shè)線程A想要通過pthread_mutex_lock操作去得到一個臨界區(qū)的鎖,而此時這個鎖正被線程B所持有,那么線程A就會被阻塞 (blocking),Core0 會在此時進(jìn)行上下文切換(Context Switch)將線程A置于等待隊(duì)列中,此時Core0就可以運(yùn)行其他的任務(wù)(例如另一個線程C)而不必進(jìn)行忙等待。而Spin lock則不然,它屬于busy-waiting類型的鎖,如果線程A是使用pthread_spin_lock操作去請求鎖,那么線程A就會一直在 Core0上進(jìn)行忙等待并不停的進(jìn)行鎖請求,直到得到這個鎖為止。
自旋鎖與互斥鎖有點(diǎn)類似,只是自旋鎖不會引起調(diào)用者睡眠,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)在那里看是 否該自旋鎖的保持者已經(jīng)釋放了鎖,"自旋"一詞就是因此而得名。其作用是為了解決某項(xiàng)資源的互斥使用。因?yàn)樽孕i不會引起調(diào)用者睡眠,所以自旋鎖的效率遠(yuǎn) 高于互斥鎖。雖然它的效率比互斥鎖高,但是它也有些不足之處:
1、自旋鎖一直占用CPU,他在未獲得鎖的情況下,一直運(yùn)行--自旋,所以占用著CPU,如果不能在很短的時 間內(nèi)獲得鎖,這無疑會使CPU效率降低。
2、在用自旋鎖時有可能造成死鎖,當(dāng)遞歸調(diào)用時有可能造成死鎖,調(diào)用有些其他函數(shù)也可能造成死鎖,如 copy_to_user()、copy_from_user()、kmalloc()等。
dispatch_semaphore
信號量是基于計(jì)數(shù)器的一種線程同步機(jī)制.
一般的如果只允許一個線程執(zhí)行代碼塊的話,那么可以使用如下方式創(chuàng)建,信號量的計(jì)數(shù)為1.
示例代碼:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_semaphore支持兩個操作:等待和通知.
等待(dispatch_semaphore_wait): 讓信號量減1,如果信號量小于0時則會一直等待,直到接收到另一個信號量通知.
通知(dispatch_semaphore_signal): 讓信號量加1, 如果之前的信號量小于0,此方法會喚起正在wait的線程,然后return.
以上是從頭文件摘錄的兩段注釋簡單翻譯過來的,原注釋應(yīng)該描述的更為清楚.
以下是通過wait和signal方式來加鎖
示例代碼:
dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);
// do something
dispatch_semaphore_signal(semaphore);
Objective-C中的鎖
Objective-C有一些常用的鎖, 他們的接口實(shí)際上都是通過NSLocking協(xié)議定義的,它定義了lock和unlock方法.你使用這些方法來獲取和釋放該鎖.
常用的鎖有:NSLock,NSRecursiveLock,NSCondition,NSConditionLock.
以NSLock為例:
NSLock *lock = [[NSLock alloc] init];
[lock lock];
// do something
[lock unlock];
上面蘋果的文檔已經(jīng)有比較詳細(xì)的描述了.
使用GCD進(jìn)行線程同步
除了上面提到的dispatch_semaphore以外,dispatch_queue也是線程同步的一種手段.
通常的我們可以使用串行隊(duì)列來進(jìn)行線程同步,比較典型的使用就是FMDB中FMDatabaseQueue的實(shí)現(xiàn).
示例代碼:
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
// do something
});
Dispatch Barrier API,也提供線程同步的一種手段. 此類API更適合實(shí)現(xiàn)讀寫鎖的機(jī)制.
示例代碼:
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
// read;
});
dispatch_barrier_async(queue, ^{
// write;
});
原子操作
iOS平臺下的原子操作函數(shù)都以O(shè)SAtomic開頭的,在<libkern/OSAtomic.h>里面.不同線程如果通過原子操作函數(shù)對同一變量進(jìn)行操作,可以保證一個線程的操作不會影響到其他線程內(nèi)對此變量的操作,因?yàn)檫@些操作都是原子式的.因?yàn)樵硬僮髦荒軐?nèi)置類型進(jìn)行操作,所以原子操作能夠同步的線程只能位于同一個進(jìn)程的地址空間內(nèi).
原子操作快速高效,常用的加/減/自增/自減等都有對應(yīng)的API實(shí)現(xiàn).
主要可以應(yīng)用到一些計(jì)數(shù)的同步中.
@synchronized關(guān)鍵字
為什么最后說它,因?yàn)檫@個關(guān)鍵字是最方便使用,也是最經(jīng)常被誤用的一種加鎖方式. 因?yàn)锧synchronized默認(rèn)添加異常處理機(jī)制,所以效率上面會有所犧牲. 這里有篇博客<More than you want to know about @synchronized>對@synchronized做了比較深入的闡述.
通常的如果不需要異常處理機(jī)制,不建議使用@synchronized,它的效率確實(shí)比較低.
小結(jié)2
至此對于iOS中常用的一些線程同步手段就介紹完了.
可能有些人會問我,我會常用哪些鎖? 個人比較偏向使用 dispatch_queue 串行隊(duì)列, 因?yàn)樗梢院唵蔚陌汛a包起來, 以免漏寫了unlock. 其他的早期偶爾會使用OSSpinLock(后來發(fā)現(xiàn)其缺陷后改用dispatch_semaphore). @synchronized會在遺留代碼中使用, 為了保持一致.
以下是讀到的一些開源代碼中一些鎖的實(shí)現(xiàn).
facebook AsyncDisplayKit 中 ASThread 的實(shí)現(xiàn);
CocoaAsyncSocket 中 GCDAsyncSocket 的實(shí)現(xiàn);
AFNetworking 中 AFURLSessionManager 的實(shí)現(xiàn);
附上自己關(guān)于上述常用鎖執(zhí)行效率的統(tǒng)計(jì)Demo,github倉庫中是一個workspace,本文中的相關(guān)內(nèi)容在Demo_Locks工程中.
對1,000,000次自增操作的加鎖結(jié)果統(tǒng)計(jì),正序排序
2016-09-25 02:48:59.593688 Demo_Locks[4110:655534] lock : NoLock, time : 2502520 ns, 0.002503 s
2016-09-25 02:48:59.593905 Demo_Locks[4110:655534] lock : SpinLock, time : 10154576 ns, 0.010155 s
2016-09-25 02:48:59.593933 Demo_Locks[4110:655534] lock : Atomic, time : 11869955 ns, 0.011870 s
2016-09-25 02:48:59.593958 Demo_Locks[4110:655534] lock : Semaphore, time : 14753828 ns, 0.014754 s
2016-09-25 02:48:59.593981 Demo_Locks[4110:655534] lock : Mutex, time : 25518736 ns, 0.025519 s
2016-09-25 02:48:59.594002 Demo_Locks[4110:655534] lock : GCD, time : 35667374 ns, 0.035667 s
2016-09-25 02:48:59.594024 Demo_Locks[4110:655534] lock : NSCondition, time : 38879077 ns, 0.038879 s
2016-09-25 02:48:59.594046 Demo_Locks[4110:655534] lock : RWLock, time : 47595821 ns, 0.047596 s
2016-09-25 02:48:59.594068 Demo_Locks[4110:655534] lock : NSLock, time : 54020795 ns, 0.054021 s
2016-09-25 02:48:59.594089 Demo_Locks[4110:655534] lock : NSRecursiveLock, time : 61731904 ns, 0.061732 s
2016-09-25 02:48:59.594112 Demo_Locks[4110:655534] lock : Synchronized, time : 109095555 ns, 0.109096 s
2016-09-25 02:48:59.594168 Demo_Locks[4110:655534] lock : NSConditionLock, time : 128421367 ns, 0.128421 s