概念
- 自旋鎖
1.1 OSSpinLock
1.2 os_unfair_lock
1.3 atomic - 互斥鎖
2.1 pthread_mutex_t
2.2 NSLock
2.3 NSRecursiveLock
2.4 @synchronized
2.5 dispatch_semaphore_t
2.6 NSCondition
2.7 NSConditionLock - 讀寫鎖
性能對比
參考
概念
什么是鎖
- 鎖是一種同步的機制探越。
- 鎖是一個對象。
- 鎖是為了保證某一個資源在同一時間搀擂,不被多個“潛在調(diào)用者”持有威恼,保證資源在同一時間不會因搶奪而出現(xiàn)錯誤。
-
鎖是對資源的訪問限制。
鎖
死鎖
當兩個及兩個以上的運算單元在等待對方停止運行哎媚,從而獲取對方持有的系統(tǒng)資源,但又沒有一方提前退出爭奪的時候,就會產(chǎn)生死鎖。
如果線程1和線程2,誰都不先釋放自己對已擁有的鎖對象的持有權(quán),那么就會陷入互相等待對方先松手的狀態(tài),這就是死鎖。
死鎖產(chǎn)生的必要條件
- 互斥條件 :
資源只能在同一時間分配給某一個運算單元涮较,如果其他的運算單元也要請求同一資源,則只能等待持有資源的運算單元使用完畢。 - 持有和等待 :
某運算單元已經(jīng)持有一個或多個資源国瓮,又請求了其他被占用的資源跟衅,這個運算單元并不釋放自己已有的資源叭莫,持有資源進行新資源的等待廓潜。 - 不可剝奪條件 :
指運算單元已經(jīng)獲得了資源,在沒有使用完成該資源的情況下,該資源不可以被剝奪,只能等待運算單元使用完畢后自己釋放。 - 循環(huán)等待 :
指一組集合中有很多運算單元佛致,它們互相持有其他運算單元的資源罐脊。
1. 自旋鎖
線程反復(fù)檢查鎖變量是否可用层玲,在此過程中線程一直處于執(zhí)行狀態(tài)润绵。適用于預(yù)期持有鎖時間很短的操作卿捎,此時因為阻塞線程和喚醒涉及上下文切換和線程數(shù)據(jù)結(jié)構(gòu)的更新,在cpu資源寬裕且鎖預(yù)期等待時間很短的情況下氛魁,輪詢通常比阻塞線程更有效宙项。
1.1 OSSpinLock(iOS 10 以后廢棄)
OSSpinLock spinLock = OS_SPINKLOCK_INIT;
OSSpinLockLock(&spinLock);
//code
OSSpinLockUnlock(&spinLock);
OSSSpinLock 存在優(yōu)先級反轉(zhuǎn)問題盆繁。如果一個低優(yōu)先級的線程 A 獲得鎖并訪問共享資源拦惋,這時如果另一個高優(yōu)先級的線程 B 也嘗試獲得這個鎖,線程 B 會處于忙等狀態(tài),由于線程 B 是一個高優(yōu)先級線程,因此 CPU 會盡量將執(zhí)行的資源分配給線程B,從而導(dǎo)致線程 B 占用大量 CPU 而線程 A 由于得到的執(zhí)行資源少而遲遲無法解鎖辅辩。
1.2 os_unfair_lock
//頭文件
@import Darwin.os.lock;
os_unfair_lock_t lock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(lock);
// code
os_unfair_lock_unlock(lock);
多個線程同時等待鎖時,先請求獲取鎖的線程不一定會先獲取鎖,鎖的獲取與請求鎖的先后順序無關(guān)赔桌,例如最后請求獲取鎖的線程可能先獲得鎖。
鎖的獲取和釋放基于原子操作。
只包含一個指針大小的內(nèi)存空間幽勒,性能開銷小击吱。
1.3 atomic
@protocol(atomic, assign) NSInteger count;
編譯器自動生成 getter / setter 內(nèi)部會調(diào)用 objc_getProperty / reallySetProperty 方法鞋仍,方法內(nèi)部根據(jù)是否為原子屬性執(zhí)行不同的代碼
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
原子性修飾的屬性會進行 spinlock 加鎖處理,spinlock由于優(yōu)先級反轉(zhuǎn)問題已經(jīng)被 os_unfair_lock 代替
using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
...
}
對于原子性修飾的屬性猫妙,只能保證 getter / setter 的線程安全,無法保證屬性在使用過程中的線程安全。例如可變數(shù)組在多個線程中 removeObjectAtIndex:
2. 互斥鎖
是一種用于多線程編程中,防止多條線程同時對同一公共資源(比如全局變量)進行讀寫的機制杖剪。它通過將代碼切片成一個一個的臨界區(qū)域達成。臨界區(qū)域指的是一塊對公共資源進行訪問的代碼,并非一種機制或是算法怠蹂。一個程序、進程、線程可以擁有多個臨界區(qū)域,但是并不一定會應(yīng)用互斥鎖妒峦。
互斥鎖不會出現(xiàn)忙碌等待,僅僅是線程阻塞坦冠。
2.1 pthread_mutex_t
// 導(dǎo)入頭文件
#import <pthread/pthread.h>
pthread_mutex_t lock;
pthread_mutexattr_t attr;
// 初始化屬性
pthread_mutexattr_init(&attr);
/*
* Mutex type attributes
#define PTHREAD_MUTEX_NORMAL 0 // 普通
#define PTHREAD_MUTEX_ERRORCHECK 1 // 此類型互斥量會自動檢測死鎖。檢查錯誤梦抢、提供錯誤提示,需要消耗一定的性能
#define PTHREAD_MUTEX_RECURSIVE 2 // 遞歸
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
*/
// 設(shè)置類型為遞歸
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化鎖灭抑,如果不需要設(shè)置屬性則第二個參數(shù)傳入NULL救湖,使用默認屬性
pthread_mutex_init(&lock, &attr);
// 銷毀屬性
pthread_mutexattr_destroy(&attr);
// 加鎖
pthread_mutex_lock(&lock);
// code
// 解鎖
pthread_mutex_unlock(&lock);
// 使用完成后需要在合適的時機對鎖進行銷毀
pthread_mutex_destroy(&lock);
pthread_mutex 是 iOS 中抵乓,多種類型的鎖的底層實現(xiàn)铡原,例如 NSLock、NSRecursiveLock晚岭、@ synchronized 字管。
pthread_mutex底層有實現(xiàn)一個阻塞隊列丁逝,如果當前有其他任務(wù)正在執(zhí)行回挽,則加入到隊列中,放棄當前cpu時間片辜膝。一旦其他任務(wù)執(zhí)行完崔梗,則從隊列中取出等待執(zhí)行的線程對象,恢復(fù)上下文重新執(zhí)行质和。
2.2 NSLock
NSLock * lock = [[NSLock alloc] init];
[lock lock];
// code
[lock unlock];
NSLock 是對 pthread_mutex 的封裝魂仍,屬性為 PTHREAD_MUTEX_ERRORCHECK赊舶,它會自動檢測死鎖,損失一定性能換來錯誤提示鸠珠。
注意:在同一個線程中多次對同一個對象加鎖會導(dǎo)致死鎖可缚。
2.3 NSRecursiveLock
NSRecursiveLock * lock = [[NSRecursiveLock alloc] init];
[lock lock];
// code
[lock unlock];
NSRecursiveLock 是對 pthread_mutex 的封裝,屬性為 PTHREAD_MUTEX_RECURSIVE
遞歸鎖允許在同一線程內(nèi)對同一個鎖對象多次加鎖允跑,但是需要注意的是在線程執(zhí)行完畢后必須在當前線程內(nèi)進行同樣次數(shù)的解鎖操作王凑,否則會導(dǎo)致其他線程無法獲得鎖(死鎖)。
- (void)NSRecursiveLockTest:(NSRecursiveLock *)lock some:(NSInteger)i {
[lock lock];
NSLog(@"%zd", i);
if (i != 0) {
[self NSRecursiveLockTest:lock some:--i];
} else {
// i == 0 時直接結(jié)束遞歸聋丝,不進行解鎖
return;
}
[lock unlock];
}
NSRecursiveLock * lock = [[NSRecursiveLock alloc] init];
// 由于首先執(zhí)行的線程沒有解鎖索烹,導(dǎo)致后面執(zhí)行的線程一直在等待解鎖
dispatch_async(self.queue, ^{
[self NSRecursiveLockTest:lock some:3];
});
dispatch_async(self.queue, ^{
[self NSRecursiveLockTest:lock some:4];
});
2.4 @synchronized
@synchronized (obj) {
// code
}
@synchronized是對pthread_mutex遞歸鎖的封裝, @synchronized(obj)內(nèi)部會生成obj對應(yīng)的遞歸鎖弱睦,然后進行加鎖百姓、解鎖操作。如果 obj 為 nil 則不會進行加鎖况木,不能保證線程安全垒拢。
2.5 dispatch_semaphore_t
// 初始化
dispatch_semaphore_t semaphore_t = dispatch_semaphore_create(0);
// 如果信號計數(shù) <= 0 阻塞當前線程旬迹,否則信號計數(shù) - 1 不阻塞當前線程
dispatch_semaphore_wait(semaphore_t,DISPATCH_TIME_FOREVER);
// 信號計數(shù) + 1
dispatch_semaphore_signal(semaphore_t);
GCD信號量是通過對信號量計數(shù)和0的對比來進行鎖的實現(xiàn)。
- 當信號量的信號計數(shù) > 0求类,使信號計數(shù) -1奔垦,并不造成線程阻塞。
- 當信號量的信號計數(shù) <= 0尸疆,其所在的線程會被阻塞執(zhí)行椿猎,直到信號計數(shù) > 0 為止。
信號量除了可以作為鎖使用還可以用于將異步操作轉(zhuǎn)為同步操作寿弱。
- (void)fetchDataComplete:(void(^)(id data, BOOL isSuccess))complete {
// 模擬網(wǎng)絡(luò)請求
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), self.queue, ^{
NSArray * data = @[@"data"];
complete(data, YES);
});
}
- (id)syncFetchData {
// 1.創(chuàng)建信號量鸵贬,計數(shù)為0
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block id data = nil;
[self fetchDataComplete:^(id d, BOOL isSuccess) {
if (isSuccess) {
data = d;
}
// 3.數(shù)據(jù)請求完成,發(fā)送 signal 信號計數(shù) + 1 繼續(xù)執(zhí)行 2
dispatch_semaphore_signal(semaphore);
}];
// 2.計數(shù) <= 0 阻塞當前線程脖捻,等待異步請求回調(diào)
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return data;
}
2.6 NSCondition
NSCondition * condition = [[NSCondition alloc] init];
// 加鎖
[condition lock];
// 解鎖
[condition unlock];
NSLock 基于 mutex 與 POSIX condition 實現(xiàn)阔逼,除了基礎(chǔ)的 lock / unlock 還支持類似于 信號量 功能:
- wait 釋放互斥量,線程進入休眠狀態(tài)地沮。
- waitUntilDate: 釋放互斥量嗜浮,當前線程立即進入休眠,其他線程繼續(xù)執(zhí)行任務(wù)摩疑,直到limit時間點危融,當前線程再被喚醒。
- signal 喚醒一個等待的線程
- broadcast 喚醒所有等待的線程
以上方法必須在 NSCondition 對象 lock 之后調(diào)用雷袋,例如下面的例子:
// 初始有兩張票
static NSInteger ticket = 2;
static NSCondition * condition;
- (void)NSConditionTest {
condition = [[NSCondition alloc] init];
for (int i = 0; i < 5; i++) {
dispatch_async(self.queue, ^{
[self waitTicket];
});
}
for (int i = 0; i < 3; i++) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(i * 3 * NSEC_PER_SEC)), self.queue, ^{
[self signalTicket];
});
}
}
- (void)waitTicket {
// 加鎖
[condition lock];
while (!ticket) {
NSLog(@"沒有票吉殃,線程休眠");
// 釋放互斥量,線程休眠
[condition wait];
// 線程喚醒后繼續(xù)while循環(huán)檢查票數(shù)楷怒,如果沒有票說明票已經(jīng)被之前喚醒的線程售出蛋勺,當前線程再次休眠
NSLog(@"線程喚醒,檢查票數(shù)");
}
ticket--;
NSLog(@"賣出了一張票 剩余:%zd", ticket);
// 解鎖
[condition unlock];
}
- (void)signalTicket {
// 加鎖
[condition lock];
ticket++;
NSLog(@"發(fā)行了一張票 剩余:%zd", ticket);
// 發(fā)送信號,通知喚醒一條休眠的線程
[condition signal];
// 解鎖
[condition unlock];
}
賣出了一張票 剩余:1
賣出了一張票 剩余:0
沒有票鸠删,線程休眠
沒有票抱完,線程休眠
沒有票,線程休眠
發(fā)行了一張票 剩余:1
線程喚醒刃泡,檢查票數(shù)
賣出了一張票 剩余:0
發(fā)行了一張票 剩余:1
線程喚醒巧娱,檢查票數(shù)
賣出了一張票 剩余:0
發(fā)行了一張票 剩余:1
線程喚醒,檢查票數(shù)
賣出了一張票 剩余:0
如果將以上例子用 mutex 與 POSIX condition 實現(xiàn)如下:
static pthread_mutex_t mutex;
static pthread_cond_t cond;
- (void)mutexAndCondTest {
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
for (int i = 0; i < 5; i++) {
dispatch_async(self.queue, ^{
[self cond_waitTicket];
});
}
for (int i = 0; i < 3; i++) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(i * 3 * NSEC_PER_SEC)), self.queue, ^{
[self cond_signalTicket];
});
}
}
- (void)cond_waitTicket {
pthread_mutex_lock(&mutex);
while (!ticket) {
NSLog(@"沒有票烘贴,線程休眠");
pthread_cond_wait(&cond, &mutex);
NSLog(@"線程喚醒禁添,檢查票數(shù)");
}
ticket--;
NSLog(@"賣出了一張票 剩余:%zd", ticket);
pthread_mutex_unlock(&mutex);
}
- (void)cond_signalTicket {
pthread_mutex_lock(&mutex);
ticket++;
NSLog(@"發(fā)行了一張票 剩余:%zd", ticket);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
2.7 NSConditionLock
// 初始化并設(shè)置條件變量
NSConditionLock * conditionLock = [[NSConditionLock alloc] initWithCondition:cond];
// 當條件變量相等且可以獲取到鎖時加鎖,否則阻塞
[conditionLock lockWhenCondition:cond];
// 只要能夠獲取到鎖就加鎖桨踪,否則阻塞
// [conditionLock lock];
// code
// 解鎖并重新設(shè)置條件變量
[conditionLock unlockWithCondition:cond];
// 解鎖 不會改變當前條件變量
// [conditionLock unlock];
NSConditionLock 是對 NSCondition 的封裝老翘,在加鎖前會比較等待變量和條件變量是否相等,如果不相等則阻塞線程
// 初始化條件變量
NSConditionLock * lock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(self.queue, ^{
// 條件變量為 1 且能夠獲取到鎖時加鎖,否則阻塞
[lock lockWhenCondition:1];
NSLog(@"1");
// 解鎖并將條件變量設(shè)置為 2
[lock unlockWithCondition:2];
});
dispatch_async(self.queue, ^{
// 條件變量為 3 且能夠獲取到鎖時時加鎖酪捡,否則阻塞
[lock lockWhenCondition:3];
NSLog(@"2");
// 解鎖并將條件變量設(shè)置為 1
[lock unlockWithCondition:1];
});
dispatch_async(self.queue, ^{
// 無論條件變量為多少只要獲取到鎖就進行加鎖,如果獲取不到鎖則阻塞線程
[lock lock];
NSLog(@"4");
// 解鎖且不改變條件變量
[lock unlock];
});
dispatch_async(self.queue, ^{
// 條件變量為 2 且能夠獲取到鎖時時加鎖纳账,否則阻塞
[lock lockWhenCondition:2];
NSLog(@"3");
// 解鎖并將條件變量設(shè)置為 3
[lock unlockWithCondition:3];
});
執(zhí)行順序: 4 -> 3 -> 2 -> 1
3. 讀寫鎖
讀寫鎖實際是一種特殊的自旋鎖逛薇,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進行讀訪問疏虫,寫者則需要對共享資源進行寫操作永罚。這種鎖相對于自旋鎖而言,能提高并發(fā)性卧秘,因為在多處理器系統(tǒng)中呢袱,它允許同時有多個讀者來訪問共享資源,最大可能的讀者數(shù)為實際的CPU數(shù)
寫者是排他性的翅敌,?個讀寫鎖同時只能有?個寫者或多個讀者(與CPU數(shù)相關(guān))羞福,但不能同時既有讀者?有寫者。在讀寫鎖保持期間也是搶占失效的
如果讀寫鎖當前沒有讀者蚯涮,也沒有寫者治专,那么寫者可以?刻獲得讀寫鎖,否則它必須?旋在那?遭顶,直到?jīng)]有任何寫者或讀者张峰。如果讀寫鎖沒有寫者,那么讀者可以?即獲得該讀寫鎖棒旗,否則讀者必須?旋在那?喘批,直到寫者釋放該讀寫鎖。
pthread_rwlock_t
// 導(dǎo)入頭文件
#import <pthread/pthread.h>
pthread_rwlock_t lock;
// 初始化讀寫鎖
pthread_rwlock_init(&lock, NULL);
// 讀操作-加鎖
pthread_rwlock_rdlock(&lock);
// 讀操作-嘗試加鎖
pthread_rwlock_tryrdlock(&lock);
// 寫操作-加鎖
pthread_rwlock_wrlock(&lock);
// 寫操作-嘗試加鎖
pthread_rwlock_trywrlock(&lock);
// 解鎖
pthread_rwlock_unlock(&lock);
// 銷毀鎖
pthread_rwlock_destroy(&lock);
讀寫鎖的三種狀態(tài) :
- 以讀的方式占據(jù)鎖的狀態(tài) :
如果有其他的線程以讀的方式請求占據(jù)鎖铣揉,并讀取鎖內(nèi)的共享資源饶深,不會造成線程阻塞,允許其他線程進行讀取逛拱,就像遞歸鎖的可重入一樣粥喜。
如果有其他的線程以寫的方式請求占據(jù)鎖,企圖更改鎖內(nèi)的共享資源橘券,則會阻塞請求的線程额湘,直到讀的操作進行完畢。
如果有其他多條線程旁舰,分別以讀和寫的不同方式請求占據(jù)鎖锋华,那么這些多條線程也會被阻塞,并且在當前線程讀操作結(jié)束后箭窜,先讓寫方式的線程占據(jù)鎖毯焕,避免讀模式的鎖長期占用資源,而寫模式的鎖卻長期堵塞。- 以寫的方式占據(jù)鎖的狀態(tài) : 所有其他請求占據(jù)鎖的線程都會阻塞纳猫。
- 沒有線程占據(jù)鎖的狀態(tài) : 按照操作系統(tǒng)的調(diào)度順序婆咸,依次調(diào)用,調(diào)度后要符合上述兩種情況芜辕。
性能對比
參考
Threading Programming Guide
【iOS】—— iOS中的相關(guān)鎖
iOS線程安全——鎖
iOS 多線程下的不同鎖
iOS常用的幾種鎖詳解以及用法
第三十二節(jié)—iOS的鎖(一)
iOS GCD (四) dispatch_semaphore 信號量