想要深入理解多線程蚀狰,鎖是預(yù)備知識,這里總結(jié)一下OC中鎖相關(guān)的知識职员,打好基礎(chǔ)麻蹋。
為什么要有鎖?
鎖概念的提出焊切,是為了解決多線程資源共享的問題扮授,在多線程環(huán)境下,有的資源可能會同時被多個線程訪問专肪,可能會出現(xiàn)資源搶奪的問題刹勃。這里引入一個概念叫臨界區(qū)(Critical Section),就是一段代碼嚎尤,同一時間只能由一個線程訪問荔仁,以保障臨界區(qū)內(nèi)的線程是安全的(資源不被搶奪,改變)芽死。鎖就是用來解決臨界區(qū)內(nèi)線程安全的問題乏梁。
下面介紹三種常見的鎖:
自旋鎖(spin lock)
自旋鎖長這樣:
while (搶鎖(lock) == 沒搶到) {
}
就是利用一個while
循環(huán),不斷的嘗試去搶鎖(這里的鎖lock
是一個抽象的概念收奔,可以是一個整數(shù)掌呜,一開始是1,表示沒有鎖坪哄,搶到后變?yōu)?质蕉,表示有鎖),搶到了鎖就跳出循環(huán)翩肌,搶不到鎖就不斷重試模暗。
自旋鎖的缺點(diǎn)很明顯,不斷的搶鎖會占用CPU資源念祭。優(yōu)點(diǎn)是線程不用休眠兑宇,不用花時間在上下文切換(context switch)從用戶態(tài)轉(zhuǎn)化為內(nèi)核態(tài),用在輕量級的臨界區(qū)上效率高粱坤。
互斥鎖(mutex)
互斥鎖長這樣:
while (搶鎖(lock) == 沒搶到) {
線程休眠隶糕,請?jiān)谶@把鎖的狀態(tài)發(fā)生改變時再喚醒(lock);
}
和自旋鎖很相似瓷产,不同就是搶不到鎖的時候,讓線程去休眠枚驻,當(dāng)鎖的狀態(tài)改變的時候再喚醒該線程濒旦。
互斥鎖的缺點(diǎn)是,線程休眠會讓線程從用戶態(tài)轉(zhuǎn)化為內(nèi)核態(tài)再登,喚醒的時候從內(nèi)核態(tài)轉(zhuǎn)化為用戶態(tài)尔邓,需要兩次上下文切換,花費(fèi)大量時間锉矢。優(yōu)點(diǎn)是不用忙等梯嗽,臨界區(qū)很長時效率高。
讀寫鎖(readers-writer lock)
讀寫鎖就是分了兩種情況沽损,一種是讀時的鎖灯节,一種是寫時的鎖,同時規(guī)定:
- 同時可以存在多個讀鎖缠俺,也就是讀-讀不互斥
- 只能存在一個寫鎖显晶,也就是讀-寫互斥贷岸,寫-寫互斥
讀寫鎖的實(shí)現(xiàn)時用了兩個互斥鎖(或者兩個自旋鎖):
//讀者加鎖
- (void)readerLock {
加鎖(rlock);
condition++;
if (condition == 1) {
加鎖(wlock);
}
解鎖(rlock);
}
//讀者解鎖
- (void)readerUnlock {
加鎖(rlock);
condition--;
if (condition == 0) {
加鎖(wlock);
}
解鎖(rlock);
}
//寫者加鎖
- (void)writerLock {
加鎖(wlock);
}
//寫者解鎖
- (void)writerUnlock {
解鎖(wlock);
}
@end
這里我們用了兩把互斥鎖(rlock
壹士,wlock
)來實(shí)現(xiàn)讀寫鎖,利用了:
- 計(jì)數(shù)器
condition
跟蹤被阻塞的讀線程偿警。如果先有寫鎖躏救,讀鎖中condition==1
,讀會被wlock
阻塞螟蒸。如果先有讀鎖盒使,寫鎖會被wlock
堵塞;讀鎖再次獲取時七嫌,可以使condition>1
少办,從而讀鎖不被堵塞。 - 互斥鎖
rlock
保護(hù)condition
诵原,供讀者使用 - 互斥鎖
wlock
確保寫操作互斥
下面介紹一個更高級的實(shí)現(xiàn)讀寫鎖的方法:條件變量+互斥鎖
首先介紹一下條件變量:
條件變量可以簡單理解為英妓,一個條件,如果達(dá)成了就發(fā)通知绍赛。這樣說有點(diǎn)抽象蔓纠,把條件變量用到讀寫鎖里就清楚了:
//讀者加鎖
- (void)readerLock {
加鎖(rwlock);
while (self.isWriting) {
解鎖,等待條件變量達(dá)成時的通知喚醒吗蚌,再加鎖(cond, rwlock);
}
self.readCount++;
解鎖(rwlock);
}
//讀者解鎖
- (void)readerUnlock {
加鎖(rwlock);
self.readCount--;
if (self.readCount == 0) {
//喚起一條寫的線程
條件變量達(dá)成時腿倚,觸發(fā)通知(cond);
}
解鎖(rwlock);
}
//寫者加鎖
- (void)writerLock {
加鎖(rwlock);
while (self.isWriting || self.readCount > 0) {
解鎖,等待條件變量達(dá)成時的通知喚醒蚯妇,再加鎖(cond, rwlock);
}
self.isWriting = YES;
解鎖(rwlock);
}
//寫者解鎖
- (void)writerUnlock {
加鎖(rwlock);
self.isWriting = NO;
//喚起多個讀的線程
條件變量達(dá)成時的敷燎,觸發(fā)通知(cond);
解鎖(rwlock);
}
@end
這里使用了使用[條件變量cond
與普通的互斥鎖rwlock
暂筝、整型計(jì)數(shù)器readCount
(表示正在讀的個數(shù))與布爾標(biāo)志isWrite
(表示正在寫)來實(shí)現(xiàn)讀寫鎖。
- 當(dāng)有讀鎖時硬贯,
readCount>0
乖杠,寫鎖進(jìn)入while
循環(huán),只有條件變量達(dá)成時澄成,才會收到通知喚醒跳出循環(huán)胧洒,條件變量達(dá)成的條件就是讀鎖全部釋放(readCount==0
)。 - 當(dāng)有寫鎖時墨状,
isWriting == YES
卫漫,讀鎖進(jìn)入while
循環(huán),只有條件變量達(dá)成時肾砂,才會收到通知喚醒出循環(huán)列赎,條件變量達(dá)成的條件就是寫鎖釋放(isWriting == NO
)。
下面總結(jié)一個OC鐘常用鎖的用法镐确,一下所有例子包吝,都用賣股票的例子:
@synchronized 關(guān)鍵字
@synchronized(這里添加一個OC對象,一般使用self) {
要加鎖的代碼
}
這是一個互斥鎖源葫,簡單易用诗越,但性能最差,建議加鎖的代碼盡量少息堂,例子如下:
- (void)viewDidLoad {
[super viewDidLoad];
//設(shè)置票的數(shù)量為5
self.tickets = 5;
self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
//線程1
dispatch_async(self.q1, ^{
[self saleTickets];
});
//線程2
dispatch_async(self.q1, ^{
[self saleTickets];
});
}
- (void)saleTickets {
while (1) {
[NSThread sleepForTimeInterval:1];
@synchronized(self) {
if (self.tickets > 0) {
self.tickets -= 1;
NSLog(@"剩余票數(shù)=%d, Thread:%@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"票賣完了 Thread:%@", [NSThread currentThread]);
break;
}
}
}
}
// 剩余票數(shù)=4, Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 剩余票數(shù)=3, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 剩余票數(shù)=2, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 剩余票數(shù)=1, Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 剩余票數(shù)=0, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 票賣完了 Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 票賣完了 Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
NSLock
_mutexLock = [[NSLock alloc] init];
[_mutexLock lock];
[_mutexLock unlock];
互斥鎖嚷狞,有lock
和unlock
方法:
- (void)viewDidLoad {
[super viewDidLoad];
//設(shè)置票的數(shù)量為5
self.tickets = 5;
self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
_mutexLock = [[NSLock alloc] init];
//線程1
dispatch_async(self.q1, ^{
[self saleTickets];
});
//線程2
dispatch_async(self.q1, ^{
[self saleTickets];
});
}
- (void)saleTickets {
while (1) {
[NSThread sleepForTimeInterval:1];
[_mutexLock lock];
if (self.tickets > 0) {
self.tickets -= 1;
NSLog(@"剩余票數(shù)=%d, Thread:%@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"票賣完了 Thread:%@", [NSThread currentThread]);
break;
}
[_mutexLock unlock];
}
}
// 剩余票數(shù)=4, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 剩余票數(shù)=3, Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}
// 剩余票數(shù)=2, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 剩余票數(shù)=1, Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}
// 剩余票數(shù)=0, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 票賣完了 Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}
pthread_mutex
pthread_mutex_init(&_mutex, NULL);
pthread_mutex_lock(&_mutex);
pthread_mutex_unlock(&_mutex);
互斥鎖
- (void)viewDidLoad {
[super viewDidLoad];
//設(shè)置票的數(shù)量為5
self.tickets = 5;
self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
// @property pthread_mutex_t mutex;
pthread_mutex_init(&_mutex, NULL);
//線程1
dispatch_async(self.q1, ^{
[self saleTickets];
});
//線程2
dispatch_async(self.q1, ^{
[self saleTickets];
});
}
- (void)saleTickets {
while (1) {
[NSThread sleepForTimeInterval:1];
pthread_mutex_lock(&_mutex);
if (self.tickets > 0) {
self.tickets -= 1;
NSLog(@"剩余票數(shù)=%d, Thread:%@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"票賣完了 Thread:%@", [NSThread currentThread]);
break;
}
pthread_mutex_unlock(&_mutex);
}
}
@end
// 剩余票數(shù)=4, Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}
// 剩余票數(shù)=3, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 剩余票數(shù)=2, Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}
// 剩余票數(shù)=1, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 剩余票數(shù)=0, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 票賣完了 Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}
dispatch_semaphore
_semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(_semaphore);
信號量實(shí)現(xiàn)加鎖,線程獲取一個信號量時荣堰,信號量數(shù)量減一床未,線程釋放信號量時,信號量數(shù)量加一振坚,信號量數(shù)量大于等于零時薇搁,加鎖的代碼可以執(zhí)行《砂耍互斥鎖可以看做是一種特殊的信號量(初始信號量等于一)啃洋。
- (void)viewDidLoad {
[super viewDidLoad];
//設(shè)置票的數(shù)量為5
self.tickets = 5;
self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
_semaphore = dispatch_semaphore_create(1);
//線程1
dispatch_async(self.q1, ^{
[self saleTickets];
});
//線程2
dispatch_async(self.q1, ^{
[self saleTickets];
});
}
- (void)saleTickets {
while (1) {
[NSThread sleepForTimeInterval:1];
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
if (self.tickets > 0) {
self.tickets -= 1;
NSLog(@"剩余票數(shù)=%d, Thread:%@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"票賣完了 Thread:%@", [NSThread currentThread]);
break;
}
dispatch_semaphore_signal(_semaphore);
}
}
@end
// 剩余票數(shù)=4, Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}
// 剩余票數(shù)=3, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 剩余票數(shù)=2, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 剩余票數(shù)=1, Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}
// 剩余票數(shù)=0, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 票賣完了 Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}
OSSpinLock
_pinLock = OS_SPINLOCK_INIT;
OSSpinLockLock(&_pinLock);
OSSpinLockUnlock(&_pinLock);
自旋鎖,效率最高呀狼,但有隱患:
可能會出現(xiàn)優(yōu)先級翻轉(zhuǎn)的情況裂允。比如線程1優(yōu)先級比較高,線程2優(yōu)先級比較低哥艇,然后在某一時刻是線程2先獲取到鎖绝编,所以先是線程2加鎖,這時候,線程1就在while(目標(biāo)鎖還未釋放)十饥,這個狀態(tài)窟勃,但因?yàn)榫€程1優(yōu)先級比較高,所以系統(tǒng)分配的時間比較多逗堵,有可能會沒有分配時間給線程2執(zhí)行后續(xù)的操作(需要做的任務(wù)和解鎖)了秉氧,這時候就會造成死鎖。
所以iOS10之后自旋鎖OSSpinLock
就被os_unfair_lock
(底層是互斥鎖)替換了蜒秤。
- (void)viewDidLoad {
[super viewDidLoad];
//設(shè)置票的數(shù)量為5
self.tickets = 5;
self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
// #import <libkern/OSAtomic.h>
// @property OSSpinLock pinLock;
// 'OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from <os/lock.h> instead
_pinLock = OS_SPINLOCK_INIT;
//線程1
dispatch_async(self.q1, ^{
[self saleTickets];
});
//線程2
dispatch_async(self.q1, ^{
[self saleTickets];
});
}
- (void)saleTickets {
while (1) {
[NSThread sleepForTimeInterval:1];
OSSpinLockLock(&_pinLock);
if (self.tickets > 0) {
self.tickets -= 1;
NSLog(@"剩余票數(shù)=%d, Thread:%@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"票賣完了 Thread:%@", [NSThread currentThread]);
break;
}
OSSpinLockUnlock(&_pinLock);
}
}
@end
// 剩余票數(shù)=4, Thread:<NSThread: 0x600000a88cc0>{number = 4, name = (null)}
// 剩余票數(shù)=3, Thread:<NSThread: 0x600000afcb00>{number = 3, name = (null)}
// 剩余票數(shù)=2, Thread:<NSThread: 0x600000a88cc0>{number = 4, name = (null)}
// 剩余票數(shù)=1, Thread:<NSThread: 0x600000afcb00>{number = 3, name = (null)}
// 剩余票數(shù)=0, Thread:<NSThread: 0x600000a88cc0>{number = 4, name = (null)}
// 票賣完了 Thread:<NSThread: 0x600000afcb00>{number = 3, name = (null)}