
前言
iOS開(kāi)發(fā)中由于各種第三方庫(kù)的高度封裝,對(duì)鎖的使用很少,剛好之前面試中被問(wèn)到的關(guān)于并發(fā)編程鎖的問(wèn)題,都是一知半解,于是決定整理一下關(guān)于iOS中鎖的知識(shí),為大家查缺補(bǔ)漏。
目錄
第一部分: 什么是鎖
第二部分: 鎖的分類(lèi)
第三部分: 性能對(duì)比
第四部分: 常見(jiàn)的死鎖
第五部分: 總結(jié)(附Demo)
正文
一、什么是鎖
在過(guò)去幾十年并發(fā)研究領(lǐng)域的出版物中,鎖總是扮演著壞人的角色,鎖背負(fù)的指控包括引起死鎖、鎖封護(hù)(luyang注:lock convoying,多個(gè)同優(yōu)先級(jí)的線程重復(fù)競(jìng)爭(zhēng)同一把鎖,此時(shí)大量雖然被喚醒而得不到鎖的線程被迫進(jìn)行調(diào)度切換,這種頻繁的調(diào)度切換相當(dāng)影響系統(tǒng)性能)、饑餓、不公平、data races以及其他許多并發(fā)帶來(lái)的罪孽。有趣的是,在共享內(nèi)存并行軟件中真正承擔(dān)重?fù)?dān)的是——你猜對(duì)了——鎖。
在計(jì)算機(jī)科學(xué)中,鎖是一種同步機(jī)制,用于多線程環(huán)境中對(duì)資源訪問(wèn)的限制。你可以理解成它用于排除并發(fā)的一種策略。
if (lock == 0) {
lock = myPID;
}
上面這段代碼并不能保證這個(gè)任務(wù)有鎖,因此它可以在同一時(shí)間被多個(gè)任務(wù)執(zhí)行。這個(gè)時(shí)候就有可能多個(gè)任務(wù)都檢測(cè)到lock是空閑的,因此兩個(gè)或者多個(gè)任務(wù)都將嘗試設(shè)置lock,而不知道其他的任務(wù)也在嘗試設(shè)置lock。這個(gè)時(shí)候就會(huì)出問(wèn)題了。再看看下面這段代碼(Swift):
class Account {
private(set) var val: Int = 0 //這里不可在其他方法修改,只能通過(guò)add/minus修改
public func add(x: Int) {
objc_sync_enter(self)
defer {
objc_sync_exit(self)
}
val += x
}
public func minus(x: Int) {
objc_sync_enter(self)
defer {
objc_sync_exit(self)
}
val -= x;
}
}
這樣就能防止多個(gè)任務(wù)去修改val了。
二、鎖的分類(lèi)
鎖根據(jù)不同的性質(zhì)可以分成不同的類(lèi)。
在WiKiPedia介紹中,一般的鎖都是建議鎖,也就四每個(gè)任務(wù)去訪問(wèn)公共資源的時(shí)候,都需要取得鎖的資訊,再根據(jù)鎖資訊來(lái)確定是否可以存取。若存取對(duì)應(yīng)資訊,鎖的狀態(tài)會(huì)改變?yōu)殒i定,因此其他線程不會(huì)訪問(wèn)該資源,當(dāng)結(jié)束訪問(wèn)時(shí),鎖會(huì)釋放,允許其他任務(wù)訪問(wèn)。有些系統(tǒng)有強(qiáng)制鎖,若未經(jīng)授權(quán)的鎖訪問(wèn)鎖定的資料,在訪問(wèn)時(shí)就會(huì)產(chǎn)生異常。
在iOS中,鎖分為互斥鎖、遞歸鎖、信號(hào)量、條件鎖、自旋鎖、讀寫(xiě)鎖(一種特所的自旋鎖)、分布式鎖。
對(duì)于數(shù)據(jù)庫(kù)的鎖分類(lèi):
| 分類(lèi)方式 | 分類(lèi) |
|---|---|
| 按鎖的粒度劃分 | 表級(jí)鎖、行級(jí)鎖、頁(yè)級(jí)鎖 |
| 按鎖的級(jí)別劃分 | 共享鎖、排他鎖 |
| 按加鎖的方式劃分 | 自動(dòng)鎖、顯示鎖 |
| 按鎖的使用方式劃分 | 樂(lè)觀鎖、悲觀鎖 |
| 按操作劃分 | DML鎖、DDL鎖 |
這里就不再詳細(xì)的介紹了,感興趣的大家可以帶Wiki
查閱相關(guān)資料。
1、互斥鎖
在編程中,引入對(duì)象互斥鎖的概念,來(lái)保證共享數(shù)據(jù)操作的完整性。每個(gè)對(duì)象都對(duì)應(yīng)于一個(gè)可稱(chēng)為“互斥鎖”的標(biāo)記,這個(gè)標(biāo)記用來(lái)保證在任一時(shí)刻,只能有一個(gè)線程訪問(wèn)對(duì)象。
1.1 @synchronized
- @synchronized要一個(gè)參數(shù),這個(gè)參數(shù)相當(dāng)于信號(hào)量
// 用在防止多線程訪問(wèn)屬性上比較多
- (void)setTestInt:(NSInteger)testInt {
@synchronized (self) {
_testInt = testInt;
}
}
1.2 NSLock
- block及宏定義
// 定義block類(lèi)型
typedef void(^MMBlock)(void);
// 定義獲取全局隊(duì)列方法
#define MM_GLOBAL_QUEUE(block) \
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ \
while (1) { \
block();\
}\
})
- 測(cè)試代碼
NSLock *lock = [[NSLock alloc] init];
MMBlock block = ^{
[lock lock];
NSLog(@"執(zhí)行操作");
sleep(1);
[lock unlock];
};
MM_GLOBAL_QUEUE(block);
1.3 pthread
pthread除了創(chuàng)建互斥鎖,還可以創(chuàng)建遞歸鎖、讀寫(xiě)鎖、once等鎖。稍后會(huì)介紹一下如何使用。如果想要深入學(xué)習(xí)pthread請(qǐng)查閱相關(guān)文檔、資料單獨(dú)學(xué)習(xí)。
靜態(tài)初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER動(dòng)態(tài)初始化:
pthread_mutex_init()函數(shù)是以動(dòng)態(tài)方式創(chuàng)建互斥鎖的,參數(shù) attr 指定了新建互斥鎖的屬性。如果參數(shù) attr 為 NULL ,使用默認(rèn)的屬性,返回0代表初始化成功。這種方式可以初始化普通鎖、遞歸鎖(同NSRecursiveLock ), 初始化方式有些復(fù)雜。此類(lèi)初始化方法可設(shè)置鎖的類(lèi)型,
PTHREAD_MUTEX_ERRORCHECK互斥鎖不會(huì)檢測(cè)死鎖,PTHREAD_MUTEX_ERRORCHECK互斥鎖可提供錯(cuò)誤檢查,PTHREAD_MUTEX_RECURSIVE遞歸鎖,PTHREAD_PROCESS_DEFAULT映射到PTHREAD_PROCESS_NORMAL.下面是我從YYKitcopy下來(lái)的:
#import <pthread.h>
//YYKit
static inline void pthread_mutex_init_recursive(pthread_mutex_t *mutex, bool recursive) {
#define YYMUTEX_ASSERT_ON_ERROR(x_) do { \
__unused volatile int res = (x_); \
assert(res == 0); \
} while (0)
assert(mutex != NULL);
if (!recursive) {
//普通鎖
YYMUTEX_ASSERT_ON_ERROR(pthread_mutex_init(mutex, NULL));
} else {
//遞歸鎖
pthread_mutexattr_t attr;
YYMUTEX_ASSERT_ON_ERROR(pthread_mutexattr_init (&attr));
YYMUTEX_ASSERT_ON_ERROR(pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE));
YYMUTEX_ASSERT_ON_ERROR(pthread_mutex_init (mutex, &attr));
YYMUTEX_ASSERT_ON_ERROR(pthread_mutexattr_destroy (&attr));
}
#undef YYMUTEX_ASSERT_ON_ERROR
}
- 測(cè)試代碼
__block pthread_mutex_t lock;
pthread_mutex_init_recursive(&lock,false);
MMBlock block0=^{
NSLog(@"線程 0:加鎖");
pthread_mutex_lock(&lock);
NSLog(@"線程 0:睡眠 1 秒");
sleep(1);
pthread_mutex_unlock(&lock);
NSLog(@"線程 0:解鎖");
};
MM_GLOBAL_QUEUE(block0);
MMBlock block1=^(){
NSLog(@"線程 1:加鎖");
pthread_mutex_lock(&lock);
NSLog(@"線程 1:睡眠 2 秒");
sleep(2);
pthread_mutex_unlock(&lock);
NSLog(@"線程 1:解鎖");
};
MM_GLOBAL_QUEUE(block1);
MMBlock block2=^{
NSLog(@"線程 2:加鎖");
pthread_mutex_lock(&lock);
NSLog(@"線程 2:睡眠 3 秒");
sleep(3);
pthread_mutex_unlock(&lock);
NSLog(@"線程 2:解鎖");
};
MM_GLOBAL_QUEUE(block2);
- 輸出結(jié)果:
線程 2:加鎖
線程 0:加鎖
線程 1:加鎖
線程 2:睡眠 3 秒
線程 2:加鎖
線程 0:加鎖
線程 1:加鎖
線程 2:睡眠 3 秒
線程 2:解鎖
線程 0:睡眠 1 秒
線程 2:加鎖
線程 2:加鎖
線程 0:加鎖
線程 1:加鎖
線程 2:睡眠 3 秒
線程 2:解鎖
線程 0:睡眠 1 秒
線程 2:加鎖
線程 0:解鎖
線程 1:睡眠 2 秒
線程 0:加鎖
2、遞歸鎖
同一個(gè)線程可以多次加鎖,不會(huì)造成死鎖
舉個(gè)??:
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveMethod(value - 1);
}
[lock unlock];
};
RecursiveMethod(5);
});
這段代碼是一個(gè)典型的死鎖情況。在我們的線程中,RecursiveMethod是遞歸調(diào)用的。所有每次進(jìn)入這個(gè)block時(shí),都會(huì)去加一次鎖,而從第二次開(kāi)始,由于鎖已經(jīng)被使用了且沒(méi)有解鎖,所有它需要等待鎖被解除,這樣就導(dǎo)致了死鎖,線程被阻塞住了??刂婆_(tái)會(huì)輸出如下信息:
value = 5
*** -[NSLock lock]: deadlock ( '(null)') *** Break on _NSLockError() to debug.
2.1 NSRecursiveLock
- 實(shí)現(xiàn)代碼
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
MM_GLOBAL_QUEUE(^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"加鎖層數(shù) %d", value);
sleep(1);
RecursiveBlock(--value);
}
[lock unlock];
};
RecursiveBlock(3);
});
- 輸出結(jié)果(從輸出結(jié)果可以看出并未發(fā)生死鎖):
加鎖層數(shù) 3
加鎖層數(shù) 2
加鎖層數(shù) 1
加鎖層數(shù) 3
加鎖層數(shù) 2
加鎖層數(shù) 1
加鎖層數(shù) 3
加鎖層數(shù) 2
2.2 pthread
- 代碼實(shí)現(xiàn)
__block pthread_mutex_t lock;
//第二個(gè)參數(shù)為true生成遞歸鎖
pthread_mutex_init_recursive(&lock,true);
MM_GLOBAL_QUEUE(^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
pthread_mutex_lock(&lock);
if (value > 0) {
NSLog(@"加鎖層數(shù) %d", value);
sleep(1);
RecursiveBlock(--value);
}
pthread_mutex_unlock(&lock);
};
RecursiveBlock(3);
});
- 輸出結(jié)果(同樣,結(jié)果顯示并未發(fā)生死鎖):
加鎖層數(shù) 3
加鎖層數(shù) 2
加鎖層數(shù) 1
加鎖層數(shù) 3
加鎖層數(shù) 2
加鎖層數(shù) 1
加鎖層數(shù) 3
加鎖層數(shù) 2
3、信號(hào)量
信號(hào)量(Semaphore),有時(shí)被稱(chēng)為信號(hào)燈,是在多線程環(huán)境下使用的一種設(shè)施,是可以用來(lái)保證兩個(gè)或多個(gè)關(guān)鍵代碼段不被并發(fā)調(diào)用。在進(jìn)入一個(gè)關(guān)鍵代碼段之前,線程必須獲取一個(gè)信號(hào)量;一旦該關(guān)鍵代碼段完成了,那么該線程必須釋放信號(hào)量。其它想進(jìn)入該關(guān)鍵代碼段的線程必須等待直到第一個(gè)線程釋放信號(hào)量
3.1 dispatch_semaphore_t
- 同步實(shí)現(xiàn)
// 參數(shù)可以理解為信號(hào)的總量,傳入的值必須大于或等于0,否則,返回NULL
// dispatch_semaphore_signal + 1
// dispatch_semaphore_wait等待信號(hào),當(dāng) <= 0會(huì)進(jìn)入等待狀態(tài)
__block dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
MM_GLOBAL_QUEUE(^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"這里簡(jiǎn)單寫(xiě)一下用法,可自行實(shí)現(xiàn)生產(chǎn)者、消費(fèi)者");
sleep(1);
dispatch_semaphore_signal(semaphore);
});
3.2 pthread
- 測(cè)試代碼
__block pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
__block pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
MM_GLOBAL_QUEUE(^{
//NSLog(@"線程 0:加鎖");
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
NSLog(@"線程 0:wait");
pthread_mutex_unlock(&mutex);
//NSLog(@"線程 0:解鎖");
});
MM_GLOBAL_QUEUE(^{
//NSLog(@"線程 1:加鎖");
sleep(3);//3秒發(fā)一次信號(hào)
pthread_mutex_lock(&mutex);
NSLog(@"線程 1:signal");
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
//NSLog(@"線程 1:加鎖");
});
4、條件鎖
3.1 NSCodition
NSCondition 的對(duì)象實(shí)際上作為一個(gè)鎖和一個(gè)線程檢查器:鎖主要為了當(dāng)檢測(cè)條件時(shí)保護(hù)數(shù)據(jù)源,執(zhí)行條件引發(fā)的任務(wù);線程檢查器主要是根據(jù)條件決定是否繼續(xù)運(yùn)行線程,即線程是否被阻塞。
- NSCondition同樣實(shí)現(xiàn)了NSLocking協(xié)議,所以它和NSLock一樣,也有NSLocking協(xié)議的lock和unlock方法,可以當(dāng)做NSLock來(lái)使用解決線程同步問(wèn)題,用法完全一樣。
- (void)getIamgeName:(NSMutableArray *)imageNames{
NSCondition *lock = [[NSCondition alloc] init];
NSString *imageName;
[lock lock];
if (imageNames.count>0) {
imageName = [imageNames lastObject];
[imageNames removeObject:imageName];
}
[lock unlock];
}
- 同時(shí),NSCondition提供更高級(jí)的用法。wait和signal,和條件信號(hào)量類(lèi)似。比如我們要監(jiān)聽(tīng)imageNames數(shù)組的個(gè)數(shù),當(dāng)imageNames的個(gè)數(shù)大于0的時(shí)候就執(zhí)行清空操作。思路是這樣的,當(dāng)imageNames個(gè)數(shù)大于0時(shí)執(zhí)行清空操作,否則,wait等待執(zhí)行清空操作。當(dāng)imageNames個(gè)數(shù)增加的時(shí)候發(fā)生signal信號(hào),讓等待的線程喚醒繼續(xù)執(zhí)行。
NSCondition和NSLock、@synchronized等是不同的是,NSCondition可以給每個(gè)線程分別加鎖,加鎖后不影響其他線程進(jìn)入臨界區(qū)。這是非常強(qiáng)大。
但是正是因?yàn)檫@種分別加鎖的方式,NSCondition使用wait并使用加鎖后并不能真正的解決資源的競(jìng)爭(zhēng)。比如我們有個(gè)需求:不能讓m<0。假設(shè)當(dāng)前m=0,線程A要判斷到m>0為假,執(zhí)行等待;線程B執(zhí)行了m=1操作,并喚醒線程A執(zhí)行m-1操作的同時(shí)線程C判斷到m>0,因?yàn)樗麄冊(cè)诓煌木€程鎖里面,同樣判斷為真也執(zhí)行了m-1,這個(gè)時(shí)候線程A和線程C都會(huì)執(zhí)行m-1,但是m=1,結(jié)果就會(huì)造成m=-1.當(dāng)我用數(shù)組做刪除試驗(yàn)時(shí),做增刪操作并不是每次都會(huì)出現(xiàn),大概3-4次后會(huì)出現(xiàn)。單純的使用lock、unlock是沒(méi)有問(wèn)題的。
- (void)executeNSCondition {
NSCondition* lock = [[NSCondition alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (NSUInteger i=0; i<3; i++) {
sleep(2);
if (i == 2) {
[lock lock];
[lock broadcast];
[lock unlock];
}
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[self threadMethodOfNSCodition:lock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[self threadMethodOfNSCodition:lock];
});
}
-(void)threadMethodOfNSCodition:(NSCondition*)lock{
[lock lock];
[lock wait];
[lock unlock];
}
3.2 NSCoditionLock
lock不分條件,如果鎖沒(méi)被申請(qǐng),直接執(zhí)行代碼unlock不會(huì)清空條件,之后滿(mǎn)足條件的鎖還會(huì)執(zhí)行unlockWithCondition:我的理解就是設(shè)置解鎖條件(同一時(shí)刻只有一個(gè)條件,如果已經(jīng)設(shè)置條件,相當(dāng)于修改條件)lockWhenCondition:滿(mǎn)足特定條件,執(zhí)行相應(yīng)代碼NSConditionLock同樣實(shí)現(xiàn)了NSLocking協(xié)議,試驗(yàn)過(guò)程中發(fā)現(xiàn)性能很低。
- NSConditionLock也可以像NSCondition一樣做多線程之間的任務(wù)等待調(diào)用,而且是線程安全的。
- (void)executeNSConditionLock {
NSConditionLock* lock = [[NSConditionLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (NSUInteger i=0; i<3; i++) {
sleep(2);
if (i == 2) {
[lock lock];
[lock unlockWithCondition:i];
}
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[self threadMethodOfNSCoditionLock:lock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
[self threadMethodOfNSCoditionLock:lock];
});
}
-(void)threadMethodOfNSCoditionLock:(NSConditionLock*)lock{
[lock lockWhenCondition:2];
[lock unlock];
}
3.3 POSIX Conditions
POSIX條件鎖需要互斥鎖和條件兩項(xiàng)來(lái)實(shí)現(xiàn),雖然看起來(lái)沒(méi)有什么關(guān)系,但在運(yùn)行時(shí)中,互斥鎖將會(huì)與條件結(jié)合起來(lái)。線程將被一個(gè)互斥和條件結(jié)合的信號(hào)來(lái)喚醒。
首先初始化條件和互斥鎖,當(dāng)
ready_to_go為false的時(shí)候,進(jìn)入循環(huán),然后線程將會(huì)被掛起,直到另一個(gè)線程將ready_to_go設(shè)置為true的時(shí)候,并且發(fā)送信號(hào)的時(shí)候,該線程才會(huì)被喚醒。測(cè)試代碼
pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean ready_to_go = true;
void MyCondInitFunction()
{
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&condition, NULL);
}
void MyWaitOnConditionFunction()
{
// Lock the mutex.
pthread_mutex_lock(&mutex);
// If the predicate is already set, then the while loop is bypassed;
// otherwise, the thread sleeps until the predicate is set.
while(ready_to_go == false)
{
pthread_cond_wait(&condition, &mutex);
}
// Do work. (The mutex should stay locked.)
// Reset the predicate and release the mutex.
ready_to_go = false;
pthread_mutex_unlock(&mutex);
}
void SignalThreadUsingCondition()
{
// At this point, there should be work for the other thread to do.
pthread_mutex_lock(&mutex);
ready_to_go = true;
// Signal the other thread to begin work.
pthread_cond_signal(&condition);
pthread_mutex_unlock(&mutex);
}
5、分布式鎖
分布式鎖是控制分布式系統(tǒng)之間同步訪問(wèn)共享資源的一種方式。在分布式系統(tǒng)中,常常需要協(xié)調(diào)他們的動(dòng)作。如果不同的系統(tǒng)或是同一個(gè)系統(tǒng)的不同主機(jī)之間共享了一個(gè)或一組資源,那么訪問(wèn)這些資源的時(shí)候,往往需要互斥來(lái)防止彼此干擾來(lái)保證一致性,在這種情況下,便需要使用到分布式鎖。
5.1 NSDistributedLock
處理多個(gè)進(jìn)程或多個(gè)程序之間互斥問(wèn)題。
一個(gè)獲取鎖的進(jìn)程或程序在是否鎖之前掛掉,鎖不會(huì)被釋放,可以通過(guò)breakLock方式解鎖。
iOS很少用到,暫不詳細(xì)研究。
6、讀寫(xiě)鎖
讀寫(xiě)鎖實(shí)際是一種特殊的自旋鎖,它把對(duì)共享資源的訪問(wèn)者劃分成讀者和寫(xiě)者,讀者只對(duì)共享資源進(jìn)行讀訪問(wèn),寫(xiě)者則需要對(duì)共享資源進(jìn)行寫(xiě)操作。這種鎖相對(duì)于自旋鎖而言,能提高并發(fā)性,因?yàn)樵诙嗵幚砥飨到y(tǒng)中,它允許同時(shí)有多個(gè)讀者來(lái)訪問(wèn)共享資源,最大可能的讀者數(shù)為實(shí)際的邏輯CPU數(shù)。寫(xiě)者是排他性的,一個(gè)讀寫(xiě)鎖同時(shí)只能有一個(gè)寫(xiě)者或多個(gè)讀者(與CPU數(shù)相關(guān)),但不能同時(shí)既有讀者又有寫(xiě)者。
6.1 dispatch_barrier_async / dispatch_barrier_sync
-
先來(lái)一個(gè)需求:假設(shè)我們?cè)扔?個(gè)任務(wù)要執(zhí)行,我們現(xiàn)在要插入一個(gè)任務(wù)0,這個(gè)任務(wù)0要在1、2、4都并發(fā)執(zhí)行完之后才能執(zhí)行,而4、5、6號(hào)任務(wù)要在這幾個(gè)任務(wù)0結(jié)束后才允許并發(fā)。大致的意思如下圖
直接上代碼:
- (void)rwLockOfBarrier {
dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"test1");
});
dispatch_async(queue, ^{
NSLog(@"test2");
});
dispatch_async(queue, ^{
NSLog(@"test3");
});
dispatch_barrier_sync(queue, ^{
for (int i = 0; i <= 500000000; i++) {
if (5000 == i) {
NSLog(@"point1");
}else if (6000 == i) {
NSLog(@"point2");
}else if (7000 == i) {
NSLog(@"point3");
}
}
NSLog(@"barrier");
});
NSLog(@"aaa");
dispatch_async(queue, ^{
NSLog(@"test4");
});
dispatch_async(queue, ^{
NSLog(@"test5");
});
dispatch_async(queue, ^{
NSLog(@"test6");
});
}
共同點(diǎn):1、等待在它前面插入隊(duì)列的任務(wù)先執(zhí)行完;2、等待他們自己的任務(wù)執(zhí)行完再執(zhí)行后面的任務(wù)。
不同點(diǎn):1、dispatch_barrier_sync將自己的任務(wù)插入到隊(duì)列的時(shí)候,需要等待自己的任務(wù)結(jié)束之后才會(huì)繼續(xù)插入被寫(xiě)在它后面的任務(wù),然后執(zhí)行它們;2、dispatch_barrier_async將自己的任務(wù)插入到隊(duì)列之后,不會(huì)等待自己的任務(wù)結(jié)束,它會(huì)繼續(xù)把后面的任務(wù)插入隊(duì)列,然后等待自己的任務(wù)結(jié)束后才執(zhí)行后面的任務(wù)。
6.2 pthread
- 與上述初始化方式類(lèi)似,靜態(tài)
THREAD_RWLOCK_INITIALIZER、動(dòng)態(tài)pthread_rwlock_init()、pthread_rwlock_destroy用來(lái)銷(xiāo)毀該鎖
#import <pthread.h>
__block pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock,NULL);
//讀
MM_GLOBAL_QUEUE(^{
//NSLog(@"線程0:隨眠 1 秒");//還是不打印能直觀些
sleep(1);
NSLog(@"線程0:加鎖");
pthread_rwlock_rdlock(&rwlock);
NSLog(@"線程0:讀");
pthread_rwlock_unlock(&rwlock);
NSLog(@"線程0:解鎖");
});
//寫(xiě)
MM_GLOBAL_QUEUE(^{
//NSLog(@"線程1:隨眠 3 秒");
sleep(3);
NSLog(@"線程1:加鎖");
pthread_rwlock_wrlock(&rwlock);
NSLog(@"線程1:寫(xiě)");
pthread_rwlock_unlock(&rwlock);
NSLog(@"線程1:解鎖");
});
7、自旋鎖
何謂自旋鎖?它是為實(shí)現(xiàn)保護(hù)共享資源而提出一種鎖機(jī)制。其實(shí),自旋鎖與互斥鎖比較類(lèi)似,它們都是為了解決對(duì)某項(xiàng)資源的互斥使用。無(wú)論是互斥鎖,還是自旋鎖,在任何時(shí)刻,最多只能有一個(gè)保持者,也就說(shuō),在任何時(shí)刻最多只能有一個(gè)執(zhí)行單元獲得鎖。但是兩者在調(diào)度機(jī)制上略有不同。對(duì)于互斥鎖,如果資源已經(jīng)被占用,資源申請(qǐng)者只能進(jìn)入睡眠狀態(tài)。但是自旋鎖不會(huì)引起調(diào)用者睡眠,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖,"自旋"一詞就是因此而得名。
7.1 OSSpinLock
- 使用方式
// 初始化
spinLock = OS_SPINKLOCK_INIT;
// 加鎖
OSSpinLockLock(&spinLock);
// 解鎖
OSSpinLockUnlock(&spinLock);
然而,YYKit作者的文章不再安全的 OSSpinLock有說(shuō)到這個(gè)自旋鎖存在優(yōu)先級(jí)反轉(zhuǎn)的問(wèn)題。
7.2 os_unfair_lock
- 自旋鎖已經(jīng)不再安全,然后蘋(píng)果又整出來(lái)個(gè) os_unfair_lock_t ,這個(gè)鎖解決了優(yōu)先級(jí)反轉(zhuǎn)的問(wèn)題。
os_unfair_lock_t unfairLock;
unfairLock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(unfairLock);
os_unfair_lock_unlock(unfairLock);
8、atomic(property) set / get
利用
set/get接口的屬性實(shí)現(xiàn)原子操作,進(jìn)而確?!氨还蚕怼钡淖兞吭诙嗑€程中讀寫(xiě)安全,這已經(jīng)是不能滿(mǎn)足部分多線程同步要求。
在定義
property的時(shí)候, 有atomic和nonatomic的屬性修飾關(guān)鍵字。對(duì)于
atomic的屬性,系統(tǒng)生成的getter/setter會(huì)保證 get、set 操作的完整性,不受其他線程影響。比如,線程 A 的 getter 方法運(yùn)行到一半,線程 B 調(diào)用了 setter:那么線程 A 的 getter 還是能得到一個(gè)完好無(wú)損的對(duì)象。而
nonatomic就沒(méi)有這個(gè)保證了。所以,nonatomic的速度要比atomic快。
Atomic
- 是默認(rèn)的
- 會(huì)保證 CPU 能在別的線程來(lái)訪問(wèn)這個(gè)屬性之前,先執(zhí)行完當(dāng)前流程
- 速度不快,因?yàn)橐WC操作整體完成
Non-Atomic
- 不是默認(rèn)的
- 更快
- 線程不安全
- 如有兩個(gè)線程訪問(wèn)同一個(gè)屬性,會(huì)出現(xiàn)無(wú)法預(yù)料的結(jié)果
假設(shè)有一個(gè) atomic 的屬性 "name",如果線程 A 調(diào)
[self setName:@"A"]`,線程 B 調(diào)[self setName:@"B"],線程 C 調(diào)[self name]``,那么所有這些不同線程上的操作都將依次順序執(zhí)行——也就是說(shuō),如果一個(gè)線程正在執(zhí)行 getter/setter,其他線程就得等待。因此,屬性 name 是讀/寫(xiě)安全的。但是,如果有另一個(gè)線程 D 同時(shí)在調(diào)
[name release],那可能就會(huì)crash,因?yàn)?release 不受 getter/setter 操作的限制。也就是說(shuō),這個(gè)屬性只能說(shuō)是讀/寫(xiě)安全的,但并不是線程安全的,因?yàn)閯e的線程還能進(jìn)行讀寫(xiě)之外的其他操作。線程安全需要開(kāi)發(fā)者自己來(lái)保證。如果 name 屬性是 nonatomic 的,那么上面例子里的所有線程 A、B、C、D 都可以同時(shí)執(zhí)行,可能導(dǎo)致無(wú)法預(yù)料的結(jié)果。如果是 atomic 的,那么 A、B、C 會(huì)串行,而 D 還是并行的。
- 簡(jiǎn)單來(lái)說(shuō),就是atomic會(huì)加一個(gè)鎖來(lái)保障線程安全,并且引用計(jì)數(shù)會(huì)+1,來(lái)向調(diào)用者保證這個(gè)對(duì)象會(huì)一直存在。假如不這樣做,如果另一個(gè)線程調(diào)setter,可能會(huì)出現(xiàn)線程競(jìng)態(tài),導(dǎo)致引用計(jì)數(shù)降到0,原來(lái)那個(gè)對(duì)象就是否了。
9、ONCE
9.1 GCD
- 多用于創(chuàng)建單例。
+ (instancetype) sharedInstance {
static id __instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
__instance = [[self alloc] init];
});
return __instance;
}
9.2 pthread
- 廢話(huà)不多說(shuō),直接上代碼
// 定義方法
void fun() {
NSLog(@"%@", [NSThread currentThread]);
}
- (void)onceOfPthread {
__block pthread_once_t once = PTHREAD_ONCE_INIT;
int i= 0;
while (i > 5) {
pthread_once(&once, fun);
i++;
}
}
三、性能對(duì)比
基礎(chǔ)表現(xiàn)-所操作耗時(shí)

上圖是常規(guī)的鎖操作性能測(cè)試(iOS7.0SDK,iPhone6模擬器,Yosemite 10.10.5),垂直方向表示耗時(shí),單位是秒,總耗時(shí)越小越好,水平方向表示不同類(lèi)型鎖的鎖操作,具體又分為兩部分,左邊的常規(guī)lock操作(比如NSLock)或者讀read操作(比如ANReadWriteLock),右邊則是寫(xiě)write操作,圖上僅有ANReadWriteLock和ANRecursiveRWLock支持,其它不支持的則默認(rèn)為0,圖上看出,單從性能表現(xiàn),原子操作是表現(xiàn)最佳的(0.057412秒),@synchronized則是最耗時(shí)的(1.753565秒) (測(cè)試代碼) 。
多線程鎖刪除數(shù)組性能測(cè)試
- 模擬器環(huán)境:i5 2.6GH+8G 內(nèi)存,xcode 7.2.1 (7C1002)+iPhone6SP(9.2)

- 真機(jī)環(huán)境:xcode 7.2.1 (7C1002)+iPhone6(國(guó)行)

通過(guò)測(cè)試發(fā)現(xiàn)模擬器和真機(jī)的區(qū)別還是很大的,模擬器上明顯的階梯感,真機(jī)就沒(méi)有,模擬器上NSConditionLock的性能非常差,我沒(méi)有把它的參數(shù)加在表格上,不然其他的就看不到了。不過(guò)真機(jī)上面性能還好。
這些性能測(cè)試只是一個(gè)參考,沒(méi)必要非要去在意這些,畢竟前端的編程一般線程要求沒(méi)那么高,可以從其他的地方優(yōu)化。線程安全中注意避坑,另外選擇自己喜歡的方式,這樣你可以研究的更深入,使用的更熟練。
聲明: 測(cè)試結(jié)果僅僅代表一個(gè)參考,因?yàn)楦鞣N因素的影響,并沒(méi)有那么準(zhǔn)確。
綜合比較

可以看到除了 OSSpinLock 外,dispatch_semaphore 和 pthread_mutex 性能是最高的。有消息稱(chēng),蘋(píng)果在新的系統(tǒng)中已經(jīng)優(yōu)化了 pthread_mutex 的性能,所有它看上去和 ** dispatch_semaphore** 差距并沒(méi)有那么大了。
四、常見(jiàn)的死鎖
首先要明確幾個(gè)概念
1.串行與并行
在使用GCD的時(shí)候,我們會(huì)把需要處理的任務(wù)放到Block中,然后將任務(wù)追加到相應(yīng)的隊(duì)列里面,這個(gè)隊(duì)列,叫做 Dispatch Queue。然而,存在于兩種Dispatch Queue,一種是要等待上一個(gè)任務(wù)執(zhí)行完,再執(zhí)行下一個(gè)的Serial Dispatch Queue,這叫做串行隊(duì)列;另一種,則是不需要上一個(gè)任務(wù)執(zhí)行完,就能執(zhí)行下一個(gè)的ConcurrentDispatch Queue,叫做并行隊(duì)列。這兩種,均遵循FIFO原則。
舉一個(gè)簡(jiǎn)單的例子,在三個(gè)任務(wù)中輸出1、2、3,串行隊(duì)列輸出是有序的1、2、3,但是并行隊(duì)列的先后順序就不一定了。
雖然可以同時(shí)多個(gè)任務(wù)的處理,但是并行隊(duì)列的處理量,還是要根據(jù)當(dāng)前系統(tǒng)狀態(tài)來(lái)。如果當(dāng)前系統(tǒng)狀態(tài)最多處理2個(gè)任務(wù),那么1、2會(huì)排在前面,3什么時(shí)候操作,就看1或者2誰(shuí)先完成,然后3接在后面。
串行和并行就簡(jiǎn)單說(shuō)到這里,關(guān)于它們的技術(shù)點(diǎn)其實(shí)還有很多,可以自行了解。
2.同步與異步
串行與并行針對(duì)的是隊(duì)列,而同步與異步,針對(duì)的則是線程。最大的區(qū)別在于,同步線程要阻塞當(dāng)前線程,必須要等待同步線程中的任務(wù)執(zhí)行完,返回以后,才能繼續(xù)執(zhí)行下一個(gè)任務(wù);而異步線程則是不用等待。
3.GCD API
GCD API很多,這里僅介紹本文用到的。
- 系統(tǒng)提供的兩個(gè)隊(duì)列
// 全局隊(duì)列,也是一個(gè)并行隊(duì)列
dispatch_get_global_queue
// 主隊(duì)列,在主線程中運(yùn)行,因?yàn)橹骶€程只有一個(gè),所有這是一個(gè)串行隊(duì)列
dispatch_get_main_queue
- 除此之外,還可以自己生成隊(duì)列
// 從DISPATCH_QUQUE_SERIAL看出,這是串行隊(duì)列
dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL)
// 同理,這是一個(gè)并行隊(duì)列
dispatch_queue_create("com.demo.concurrentQueue", DISPATCH_QUEUE_CONCURRENT)
- 接下來(lái)是同步與異步線程的創(chuàng)造
dispatch_sync(..., ^(block)) // 同步線程
dispatch_async(..., ^(block)) // 異步線程
案例分析
案例一
NSLog(@"1"); // 任務(wù)1
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2"); // 任務(wù)2
});
NSLog(@"3"); // 任務(wù)3
- 結(jié)果,控制臺(tái)輸出:
1
分析
- dispatch_sync表示一個(gè)同步線程;
- dispatch_get_main_queue表示運(yùn)行在主線程中的主隊(duì)列;
- 任務(wù)2是同步線程的任務(wù)。
首先執(zhí)行任務(wù)1,這是肯定沒(méi)問(wèn)題的,只是接下來(lái),程序遇到了同步線程,那么它會(huì)進(jìn)入等待,等待任務(wù)2執(zhí)行完,然后執(zhí)行任務(wù)3。但這是隊(duì)列,有任務(wù)來(lái),當(dāng)然會(huì)將任務(wù)加到隊(duì)尾,然后遵循FIFO原則執(zhí)行任務(wù)。那么,現(xiàn)在任務(wù)2就會(huì)被加到最后,任務(wù)3排在了任務(wù)2前面,問(wèn)題來(lái)了:
任務(wù)3要等任務(wù)2執(zhí)行完才能執(zhí)行,任務(wù)2由排在任務(wù)3后面,意味著任務(wù)2要在任務(wù)3執(zhí)行完才能執(zhí)行,所以他們進(jìn)入了互相等待的局面?!炯热贿@樣,那干脆就卡在這里吧】這就是死鎖。

案例二
NSLog(@"1"); // 任務(wù)1
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"2"); // 任務(wù)2
});
NSLog(@"3"); // 任務(wù)3
- 結(jié)果,控制臺(tái)輸出:
1
2
3
分析
首先執(zhí)行任務(wù)1,接下來(lái)會(huì)遇到一個(gè)同步線程,程序會(huì)進(jìn)入等待。等待任務(wù)2執(zhí)行完成以后,才能繼續(xù)執(zhí)行任務(wù)3。從dispatch_get_global_queue可以看出,任務(wù)2被加入到了全局的并行隊(duì)列中,當(dāng)并行隊(duì)列執(zhí)行完任務(wù)2以后,返回到主隊(duì)列,繼續(xù)執(zhí)行任務(wù)3。

案例三
dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1"); // 任務(wù)1
dispatch_async(queue, ^{
NSLog(@"2"); // 任務(wù)2
dispatch_sync(queue, ^{
NSLog(@"3"); // 任務(wù)3
});
NSLog(@"4"); // 任務(wù)4
});
NSLog(@"5"); // 任務(wù)5
- 結(jié)果,控制臺(tái)輸出:
1
5
2
// 5和2的順序不一定
分析
這個(gè)案例沒(méi)有使用系統(tǒng)提供的串行或并行隊(duì)列,而是自己通過(guò)dispatch_queue_create函數(shù)創(chuàng)建了一個(gè)DISPATCH_QUEUE_SERIAL的串行隊(duì)列。
- 執(zhí)行任務(wù)1;
- 遇到異步線程,將【任務(wù)2、同步線程、任務(wù)4】加入串行隊(duì)列中。因?yàn)槭钱惒骄€程,所以在主線程中的任務(wù)5不必等待異步線程中的所有任務(wù)完成;
- 因?yàn)槿蝿?wù)5不必等待,所以2和5的輸出順序不能確定;
- 任務(wù)2執(zhí)行完以后,遇到同步線程,這時(shí),將任務(wù)3加入串行隊(duì)列;
- 又因?yàn)槿蝿?wù)4比任務(wù)3早加入串行隊(duì)列,所以,任務(wù)3要等待任務(wù)4完成以后,才能執(zhí)行。但是任務(wù)3所在的同步線程會(huì)阻塞,所以任務(wù)4必須等任務(wù)3執(zhí)行完以后再執(zhí)行。這就又陷入了無(wú)限的等待中,造成死鎖。

案例四
NSLog(@"1"); // 任務(wù)1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2"); // 任務(wù)2
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"3"); // 任務(wù)3
});
NSLog(@"4"); // 任務(wù)4
});
NSLog(@"5"); // 任務(wù)5
- 結(jié)果,控制臺(tái)輸出:
1
2
5
3
4
// 5和2的順序不一定
分析
首先,將【任務(wù)1、異步線程、任務(wù)5】加入Main Queue中,異步線程中的任務(wù)是:【任務(wù)2、同步線程、任務(wù)4】。
所以,先執(zhí)行任務(wù)1,然后將異步線程中的任務(wù)加入到Global Queue中,因?yàn)楫惒骄€程,所以任務(wù)5不用等待,結(jié)果就是2和5的輸出順序不一定。
然后再看異步線程中的任務(wù)執(zhí)行順序。任務(wù)2執(zhí)行完以后,遇到同步線程。將同步線程中的任務(wù)加入到Main Queue中,這時(shí)加入的任務(wù)3在任務(wù)5的后面。
當(dāng)任務(wù)3執(zhí)行完以后,沒(méi)有了阻塞,程序繼續(xù)執(zhí)行任務(wù)4。
從以上的分析來(lái)看,得到的幾個(gè)結(jié)果:1最先執(zhí)行;2和5順序不一定;4一定在3后面。

案例五
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1"); // 任務(wù)1
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2"); // 任務(wù)2
});
NSLog(@"3"); // 任務(wù)3
});
NSLog(@"4"); // 任務(wù)4
while (1) {
}
NSLog(@"5"); // 任務(wù)5
- 結(jié)果,控制臺(tái)輸出:
1
4
// 1和4的順序不一定
分析
和上面幾個(gè)案例的分析類(lèi)似,先來(lái)看看都有哪些任務(wù)加入了Main Queue:【異步線程、任務(wù)4、死循環(huán)、任務(wù)5】。
在加入到Global Queue異步線程中的任務(wù)有:【任務(wù)1、同步線程、任務(wù)3】。
第一個(gè)就是異步線程,任務(wù)4不用等待,所以結(jié)果任務(wù)1和任務(wù)4順序不一定。
任務(wù)4完成后,程序進(jìn)入死循環(huán),Main Queue阻塞。但是加入到Global Queue的異步線程不受影響,繼續(xù)執(zhí)行任務(wù)1后面的同步線程。
同步線程中,將任務(wù)2加入到了主線程,并且,任務(wù)3等待任務(wù)2完成以后才能執(zhí)行。這時(shí)的主線程,已經(jīng)被死循環(huán)阻塞了。所以任務(wù)2無(wú)法執(zhí)行,當(dāng)然任務(wù)3也無(wú)法執(zhí)行,在死循環(huán)后的任務(wù)5也不會(huì)執(zhí)行。
最終,只能得到1和4順序不定的結(jié)果。

五、總結(jié)
- 總的來(lái)看,推薦pthread_mutex作為實(shí)際項(xiàng)目的首選方案;
- 對(duì)于耗時(shí)較大又易沖突的讀操作,可以使用讀寫(xiě)鎖代替pthread_mutex;
- 如果確認(rèn)僅有set/get的訪問(wèn)操作,可以選用原子操作屬性;
- 對(duì)于性能要求苛刻,可以考慮使用OSSpinLock,需要確保加鎖片段的耗時(shí)足夠??;
- 條件鎖基本上使用面向?qū)ο蟮腘SCondition和NSConditionLock即可;
- @synchronized則適用于低頻場(chǎng)景如初始化或者緊急修復(fù)使用;
蘋(píng)果為多線程、共享內(nèi)存提供了多種同步解決方案(鎖),對(duì)于這些方案的比較,大都討論了鎖的用法以及鎖操作的開(kāi)銷(xiāo)。個(gè)人認(rèn)為最優(yōu)秀的選用還是看應(yīng)用場(chǎng)景,高頻接口VS低頻接口、有限沖突VS激烈競(jìng)爭(zhēng)、代碼片段耗時(shí)的長(zhǎng)短,都是選擇的重要依據(jù),選擇適用于當(dāng)前應(yīng)用場(chǎng)景的方案才是王道。
最后,由于時(shí)間匆促,如果有錯(cuò)誤或者不足的地方請(qǐng)指正,最后附上Demo所有代碼的集合,下面是我的github和博客。
聯(lián)系方式:ed_sun0129@163.com