多線程,作為實現(xiàn)軟件并發(fā)執(zhí)行的一個重要的方法,也開始具有越來越重要的地位!

正式因為多線程能夠在時間片里被CPU快速切換,造就了以下優(yōu)勢
- 資源利用率更好
- 程序設(shè)計在某些情況下更簡單
- 程序響應(yīng)更快
但是并不是非常完美,因為多線程常常伴有資源搶奪的問題,作為一個高級開發(fā)人員并發(fā)編程那是必須要的,同時解決線程安全也成了我們必須要要掌握的基礎(chǔ)
原子操作
自旋鎖其實就是封裝了一個spinlock_t自旋鎖
自旋鎖:如果共享數(shù)據(jù)已經(jīng)有其他線程加鎖了,線程會以死循環(huán)的方式等待鎖,一旦被訪問的資源被解鎖,則等待資源的線程會立即執(zhí)行。自旋鎖下面還會展開來介紹
互斥鎖:如果共享數(shù)據(jù)已經(jīng)有其他線程加鎖了,線程會進入休眠狀態(tài)等待鎖。一旦被訪問的資源被解鎖,則等待資源的線程會被喚醒。
下面是自旋鎖的實現(xiàn)原理:
bool lock = false; // 一開始沒有鎖上,任何線程都可以申請鎖
do {
while(test_and_set(&lock); // test_and_set 是一個原子操作
Critical section // 臨界區(qū)
lock = false; // 相當(dāng)于釋放鎖,這樣別的線程可以進入臨界區(qū)
Reminder section // 不需要鎖保護的代碼
}
這里有一篇關(guān)于原子性的比較有意思的文章,這里也貼出來,大家可以一起交流討論
為什么說atomic有時候無法保證線程安全呢?
不再安全的 OSSpinLock
操作在底層會被編譯為匯編代碼之后不止一條指令,因此在執(zhí)行的時候可能執(zhí)行了一半就被調(diào)度系統(tǒng)打斷,去執(zhí)行別的代碼,而我們的原子性的單條指令的執(zhí)行是不會被打斷的,所以保證了安全.
自旋鎖的BUG
盡管原子操作非常的簡單,但是它只適合于比較簡單特定的場合。在復(fù)雜的場合下,比如我們要保證一個復(fù)雜的數(shù)據(jù)結(jié)構(gòu)更改的原子性,原子操作指令就力不從心了,
如果臨界區(qū)的執(zhí)行時間過長,使用自旋鎖不是個好主意。之前我們介紹過時間片輪轉(zhuǎn)算法,線程在多種情況下會退出自己的時間片。其中一種是用完了時間片的時間,被操作系統(tǒng)強制搶占。除此以外,當(dāng)線程進行 I/O 操作,或進入睡眠狀態(tài)時,都會主動讓出時間片。顯然在 while 循環(huán)中,線程處于忙等狀態(tài),白白浪費 CPU 時間,最終因為超時被操作系統(tǒng)搶占時間片。如果臨界區(qū)執(zhí)行時間較長,比如是文件讀寫,這種忙等是毫無必要的
下面開始我們又愛又恨的鎖
iOS鎖
大家也可以參考這篇文章進行拓展:iOS鎖
鎖并是一種非強制機制,每一個現(xiàn)貨出呢個在訪問數(shù)據(jù)或資源之前視圖獲?。ˋcquire)鎖,并在訪問結(jié)束之后釋放(Release)鎖。在鎖已經(jīng)被占用的時候試圖獲取鎖,線程會等待,知道鎖重新可用!
信號量
二元信號量(Binary Semaphore)只有兩種狀態(tài):占用與非占用。它適合被唯一一個線程獨占訪問的資源。當(dāng)二元信號量處于非占用狀態(tài)時,第一個試圖獲取該二元信號量的線程會獲得該鎖,并將二元信號量置為占用狀態(tài),伺候其他的所有試圖獲取該二元信號量的線程將會等待,直到該鎖被釋放
現(xiàn)在我們在這個基礎(chǔ)上,我們把學(xué)習(xí)的思維由二元->多元的時候,我們的信號量由此誕生,多元信號量簡稱信號量
將信號量的值減1
如果信號量的值小于0,則進入等待狀態(tài),否則繼續(xù)執(zhí)行。訪問玩資源之后,線程釋放信號量,進行如下操作
將信號量的值加1
如果信號量的值小于1,喚醒一個等待中的線程
let sem = DispatchSemaphore(value: 1)
for index in 1...5 {
DispatchQueue.global().async {
sem.wait()
print(index,Thread.current)
sem.signal()
}
}
輸出結(jié)果:
1 <NSThread: 0x600003fa8200>{number = 3, name = (null)}
2 <NSThread: 0x600003f90140>{number = 4, name = (null)}
3 <NSThread: 0x600003f94200>{number = 5, name = (null)}
4 <NSThread: 0x600003fa0940>{number = 6, name = (null)}
5 <NSThread: 0x600003f94240>{number = 7, name = (null)}
互斥量
互斥量(Mutex)又叫互斥鎖和二元信號量很類似,但和信號量不同的是,信號量在整個系統(tǒng)可以被任意線程獲取并釋放;也就是說哪個線程鎖的,要哪個線程釋放鎖。
具體詳細(xì)的用法可以參考:常見鎖用法
Mutex可以分為遞歸鎖(recursive mutex)和非遞歸鎖(non-recursive mutex)。 遞歸鎖也叫可重入鎖(reentrant mutex),非遞歸鎖也叫不可重入鎖(non-reentrant mutex)。
二者唯一的區(qū)別是:
- 同一個線程可以多次獲取同一個遞歸鎖,不會產(chǎn)生死鎖。
- 如果一個線程多次獲取同一個非遞歸鎖,則會產(chǎn)生死鎖。
NSLock 是最簡單額互斥鎖!但是是非遞歸的!直接封裝了pthread_mutex 用法非常簡單就不做贅述
@synchronized 是我們互斥鎖里面用的最頻繁的,但是性能最差!
int main(int argc, const char * argv[]) {
NSString *obj = @"Iceberg";
@synchronized(obj) {
NSLog(@"Hello,world! => %@" , obj);
}
}
底層clang
int main(int argc, const char * argv[]) {
NSString *obj = (NSString *)&__NSConstantStringImpl__var_folders_8l_rsj0hqpj42b9jsw81mc3xv_40000gn_T_block_main_54f70c_mi_0;
{
id _rethrow = 0;
id _sync_obj = (id)obj;
objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT {
_SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {
objc_sync_exit(sync_exit);
}
id sync_exit;
} _sync_exit(_sync_obj);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8l_rsj0hqpj42b9jsw81mc3xv_40000gn_T_block_main_54f70c_mi_1 , obj);
} catch (id e) {
_rethrow = e;
}
{
struct _FIN {
_FIN(id reth) : rethrow(reth) {}
~_FIN() {
if (rethrow)
objc_exception_throw(rethrow);
}
id rethrow;
} _fin_force_rethow(_rethrow);
}
}
}
我們發(fā)現(xiàn)
objc_sync_enter函數(shù)是在try語句之前調(diào)用,參數(shù)為需要加鎖的對象。因為C++中沒有try{}catch{}finally{}語句,所以不能在finally{}調(diào)用objc_sync_exit函數(shù)。因此objc_sync_exit是在_SYNC_EXIT結(jié)構(gòu)體中的析構(gòu)函數(shù)中調(diào)用,參數(shù)同樣是當(dāng)前加鎖的對象。這個設(shè)計很巧妙,原因在_SYNC_EXIT結(jié)構(gòu)體類型的_sync_exit是一個局部變量,生命周期為try{}語句塊,其中包含了@sychronized{}代碼需要執(zhí)行的代碼,在代碼完成后,_sync_exit局部變量出棧釋放,隨即調(diào)用其析構(gòu)函數(shù),進而調(diào)用objc_sync_exit函數(shù)。即使try{}語句塊中的代碼執(zhí)行過程中出現(xiàn)異常,跳轉(zhuǎn)到catch{}語句,局部變量_sync_exit同樣會被釋放,完美的模擬了finally的功能。
由于篇幅原因,這里分享一篇非常不錯的博客:底層分析synchronized
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");
result = recursive_mutex_lock(&data->mutex);
require_noerr_string(result, done, "mutex_lock failed");
} 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();
}
done:
return result;
}
從上面的源碼中我們可以得出你調(diào)用sychronized的每個對象,Objective-C runtime都會為其分配一個遞歸鎖并存儲在哈希表中。完美
其實如果大家覺得@sychronized性能低的話,完全可以用NSRecursiveLock現(xiàn)成的封裝好的遞歸鎖
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value:%d", value);
RecursiveBlock(value - 1);
}
[lock unlock];
};
RecursiveBlock(2);
});
2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:2
2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:1
條件變量
條件變量(Condition Variable)作為一種同步手段,作用類似一個柵欄。對于條件變量,現(xiàn)成可以有兩種操作:
- 首先線程可以等待條件變量,一個條件變量可以被多個線程等待
- 其次線程可以喚醒條件變量。此時某個或所有等待此條件變量的線程都會被喚醒并繼續(xù)支持。
換句話說:使用條件變量可以讓許多線程一起等待某個時間的發(fā)生,當(dāng)某個時間發(fā)生時,所有的線程可以一起恢復(fù)執(zhí)行!
相信仔細(xì)的大家肯定在鎖的用法里面見過NSCondition,就是封裝了條件變量pthread_cond_t和互斥鎖
- (void) signal {
pthread_cond_signal(&_condition);
}
// 其實這個函數(shù)是通過宏來定義的,展開后就是這樣
- (void) lock {
int err = pthread_mutex_lock(&_mutex);
}
NSConditionLock借助 NSCondition來實現(xiàn),它的本質(zhì)就是一個生產(chǎn)者-消費者模型?!皸l件被滿足”可以理解為生產(chǎn)者提供了新的內(nèi)容。NSConditionLock 的內(nèi)部持有一個NSCondition對象,以及 _condition_value屬性,在初始化時就會對這個屬性進行賦值:
// 簡化版代碼
- (id) initWithCondition: (NSInteger)value {
if (nil != (self = [super init])) {
_condition = [NSCondition new]
_condition_value = value;
}
return self;
}
臨界區(qū)
比互斥量更加嚴(yán)格的同步手段。在術(shù)語中,把臨界區(qū)的獲取稱為進入臨界區(qū),而把鎖的釋放稱為離開臨界區(qū)。與互斥量和信號量的區(qū)別:
- (1)互斥量和信號量字系統(tǒng)的任何進程都是可見的。
- (2)臨界區(qū)的作用范圍僅限于本進程,其他進程無法獲取該鎖。
// 臨界區(qū)結(jié)構(gòu)對象
CRITICAL_SECTION g_cs;
// 共享資源
char g_cArray[10];
UINT ThreadProc10(LPVOID pParam)
{
// 進入臨界區(qū)
EnterCriticalSection(&g_cs);
// 對共享資源進行寫入操作
for (int i = 0; i < 10; i++)
{
g_cArray[i] = a;
Sleep(1);
}
// 離開臨界區(qū)
LeaveCriticalSection(&g_cs);
return 0;
}
UINT ThreadProc11(LPVOID pParam)
{
// 進入臨界區(qū)
EnterCriticalSection(&g_cs);
// 對共享資源進行寫入操作
for (int i = 0; i < 10; i++)
{
g_cArray[10 - i - 1] = b;
Sleep(1);
}
// 離開臨界區(qū)
LeaveCriticalSection(&g_cs);
return 0;
}
……
void CSample08View::OnCriticalSection()
{
// 初始化臨界區(qū)
InitializeCriticalSection(&g_cs);
// 啟動線程
AfxBeginThread(ThreadProc10, NULL);
AfxBeginThread(ThreadProc11, NULL);
// 等待計算完畢
Sleep(300);
// 報告計算結(jié)果
CString sResult = CString(g_cArray);
AfxMessageBox(sResult);
}
讀寫鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);
ReadWriteLock管理一組鎖,一個是只讀的鎖,一個是寫鎖。讀鎖可以在沒有寫鎖的時候被多個線程同時持有,寫鎖是獨占的。
#include <pthread.h> //多線程、讀寫鎖所需頭文件
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; //定義和初始化讀寫鎖
寫模式:
pthread_rwlock_wrlock(&rwlock); //加寫鎖
寫寫寫……
pthread_rwlock_unlock(&rwlock); //解鎖
讀模式:
pthread_rwlock_rdlock(&rwlock); //加讀鎖
讀讀讀……
pthread_rwlock_unlock(&rwlock); //解鎖
- 用條件變量實現(xiàn)讀寫鎖
這里用條件變量+互斥鎖來實現(xiàn)。注意:條件變量必須和互斥鎖一起使用,等待、釋放的時候都需要加鎖。
#include <pthread.h> //多線程、互斥鎖所需頭文件
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //定義和初始化互斥鎖
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //定義和初始化條件變量
寫模式:
pthread_mutex_lock(&mutex); //加鎖
while(w != 0 || r > 0)
{
pthread_cond_wait(&cond, &mutex); //等待條件變量的成立
}
w = 1;
pthread_mutex_unlock(&mutex);
寫寫寫……
pthread_mutex_lock(&mutex);
w = 0;
pthread_cond_broadcast(&cond); //喚醒其他因條件變量而產(chǎn)生的阻塞
pthread_mutex_unlock(&mutex); //解鎖
讀模式:
pthread_mutex_lock(&mutex);
while(w != 0)
{
pthread_cond_wait(&cond, &mutex); //等待條件變量的成立
}
r++;
pthread_mutex_unlock(&mutex);
讀讀讀……
pthread_mutex_lock(&mutex);
r- -;
if(r == 0)
pthread_cond_broadcast(&cond); //喚醒其他因條件變量而產(chǎn)生的阻塞
pthread_mutex_unlock(&mutex); //解鎖
- 用互斥鎖實現(xiàn)讀寫鎖
這里使用2個互斥鎖+1個整型變量來實現(xiàn)
#include <pthread.h> //多線程、互斥鎖所需頭文件
pthread_mutex_t r_mutex = PTHREAD_MUTEX_INITIALIZER; //定義和初始化互斥鎖
pthread_mutex_t w_mutex = PTHREAD_MUTEX_INITIALIZER;
int readers = 0; //記錄讀者的個數(shù)
寫模式:
pthread_mutex_lock(&w_mutex);
寫寫寫……
pthread_mutex_unlock(&w_mutex);
讀模式:
pthread_mutex_lock(&r_mutex);
if(readers == 0)
pthread_mutex_lock(&w_mutex);
readers++;
pthread_mutex_unlock(&r_mutex);
讀讀讀……
pthread_mutex_lock(&r_mutex);
readers- -;
if(reader == 0)
pthread_mutex_unlock(&w_mutex);
pthread_mutex_unlock(&r_mutex);
- 用信號量來實現(xiàn)讀寫鎖
這里使用2個信號量+1個整型變量來實現(xiàn)。令信號量的初始數(shù)值為1,那么信號量的作用就和互斥量等價了。
#include <semaphore.h> //線程信號量所需頭文件
sem_t r_sem; //定義信號量
sem_init(&r_sem, 0, 1); //初始化信號量
sem_t w_sem; //定義信號量
sem_init(&w_sem, 0, 1); //初始化信號量
int readers = 0;
寫模式:
sem_wait(&w_sem);
寫寫寫……
sem_post(&w_sem);
讀模式:
sem_wait(&r_sem);
if(readers == 0)
sem_wait(&w_sem);
readers++;
sem_post(&r_sem);
讀讀讀……
sem_wait(&r_sem);
readers- -;
if(readers == 0)
sem_post(&w_sem);
sem_post(&r_sem);
線程的安全是現(xiàn)在各個領(lǐng)域在多線程開發(fā)必須要掌握的基礎(chǔ)!只有對底層有所掌握,才能在真正的實際開發(fā)中游刃有余!現(xiàn)在的iOS開發(fā)乃至其他開發(fā)都是表面基礎(chǔ)層開發(fā),真正大牛開發(fā)是必須要掌握的,這一篇博客以供大家一起學(xué)習(xí)!