多執行緒,作為實現軟體併發執行的一個重要的方法,也開始具有越來越重要的地位!
正式因為多執行緒能夠在時間片裡被CPU快速切換,造就了以下優勢
- 資源利用率更好
- 程式設計在某些情況下更簡單
- 程式響應更快
但是並不是非常完美,因為多執行緒常常伴有資源搶奪的問題,作為一個高階開發人員併發程式設計那是必須要的,同時解決執行緒安全也成了我們必須要要掌握的基礎
原子操作
自旋鎖其實就是封裝了一個spinlock_t
自旋鎖
自旋鎖:如果共享資料已經有其他執行緒加鎖了,執行緒會以死迴圈的方式等待鎖,一旦被訪問的資源被解鎖,則等待資源的執行緒會立即執行。自旋鎖下面還會展開來介紹
互斥鎖:如果共享資料已經有其他執行緒加鎖了,執行緒會進入休眠狀態等待鎖。一旦被訪問的資源被解鎖,則等待資源的執行緒會被喚醒。
下面是自旋鎖的實現原理:
bool lock = false; // 一開始沒有鎖上,任何執行緒都可以申請鎖
do {
while(test_and_set(&lock); // test_and_set 是一個原子操作
Critical section // 臨界區
lock = false; // 相當於釋放鎖,這樣別的執行緒可以進入臨界區
Reminder section // 不需要鎖保護的程式碼
}
複製程式碼
這裡有一篇關於原子性的比較有意思的文章,這裡也貼出來,大家可以一起交流討論 為什麼說atomic有時候無法保證執行緒安全呢? 不再安全的 OSSpinLock
操作在底層會被編譯為彙編程式碼之後不止一條指令,因此在執行的時候可能執行了一半就被排程系統打斷,去執行別的程式碼,而我們的原子性的單條指令的執行是不會被打斷的,所以保證了安全.
自旋鎖的BUG
儘管原子操作非常的簡單,但是它只適合於比較簡單特定的場合。在複雜的場合下,比如我們要保證一個複雜的資料結構更改的原子性,原子操作指令就力不從心了,
如果臨界區的執行時間過長,使用自旋鎖不是個好主意。之前我們介紹過時間片輪轉演算法,執行緒在多種情況下會退出自己的時間片。其中一種是用完了時間片的時間,被作業系統強制搶佔。除此以外,當執行緒進行 I/O 操作,或進入睡眠狀態時,都會主動讓出時間片。顯然在 while 迴圈中,執行緒處於忙等狀態,白白浪費 CPU 時間,最終因為超時被作業系統搶佔時間片。如果臨界區執行時間較長,比如是檔案讀寫,這種忙等是毫無必要的
下面開始我們又愛又恨的鎖
iOS鎖
大家也可以參考這篇文章進行擴充:iOS鎖
鎖並是一種非強制機制,每一個現貨出呢個在訪問資料或資源之前檢視**獲取(Acquire)鎖,並在訪問結束之後釋放(Release)**鎖。在鎖已經被佔用的時候試圖獲取鎖,執行緒會等待,知道鎖重新可用!
訊號量
**二元訊號量(Binary Semaphore)**只有兩種狀態:佔用與非佔用。它適合被唯一一個執行緒獨佔訪問的資源。當二元訊號量處於非佔用狀態時,第一個試圖獲取該二元訊號量的執行緒會獲得該鎖,並將二元訊號量置為佔用狀態,伺候其他的所有試圖獲取該二元訊號量的執行緒將會等待,直到該鎖被釋放
現在我們在這個基礎上,我們把學習的思維由二元->多元的時候,我們的訊號量由此誕生,多元訊號量簡稱訊號量
-
將訊號量的值減1
-
如果訊號量的值小於0,則進入等待狀態,否則繼續執行。訪問玩資源之後,執行緒釋放訊號量,進行如下操作
-
將訊號量的值加1
-
如果訊號量的值小於1,喚醒一個等待中的執行緒
let sem = DispatchSemaphore(value: 1)
for index in 1...5 {
DispatchQueue.global().async {
sem.wait()
print(index,Thread.current)
sem.signal()
}
}
輸出結果:
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)又叫互斥鎖和二元訊號量很類似,但和訊號量不同的是,訊號量在整個系統可以被任意執行緒獲取並釋放;也就是說哪個執行緒鎖的,要哪個執行緒釋放鎖。
具體詳細的用法可以參考:常見鎖用法
Mutex
可以分為遞迴鎖(recursive mutex)
和非遞迴鎖(non-recursive mutex)
。 遞迴鎖也叫可重入鎖(reentrant mutex)
,非遞迴鎖也叫不可重入鎖(non-reentrant mutex)
。 二者唯一的區別是:
- 同一個執行緒可以多次獲取同一個遞迴鎖,不會產生死鎖。
- 如果一個執行緒多次獲取同一個非遞迴鎖,則會產生死鎖。
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);
}
}
}
複製程式碼
我們發現
objc_sync_enter
函式是在try
語句之前呼叫,引數為需要加鎖的物件。因為C++
中沒有try{}catch{}finally{}
語句,所以不能在finally{}
呼叫objc_sync_exit
函式。因此objc_sync_exit
是在_SYNC_EXIT
結構體中的解構函式
中呼叫,引數同樣是當前加鎖的物件。這個設計很巧妙,原因在_SYNC_EXIT
結構體型別的_sync_exit
是一個區域性變數,生命週期為try{}
語句塊,其中包含了@sychronized{}
程式碼需要執行的程式碼,在程式碼完成後,_sync_exit
區域性變數出棧釋放,隨即呼叫其解構函式,進而呼叫objc_sync_exit
函式。即使try{}
語句塊中的程式碼執行過程中出現異常,跳轉到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;
}
複製程式碼
從上面的原始碼中我們可以得出你呼叫sychronized
的每個物件,Objective-C runtime
都會為其分配一個遞迴鎖並儲存在雜湊表中。完美
其實如果大家覺得@sychronized
效能低的話,完全可以用NSRecursiveLock
現成的封裝好的遞迴鎖
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)
作為一種同步手段,作用類似一個柵欄。對於條件變數,現成可以有兩種操作:
- 首先執行緒可以等待條件變數,一個條件變數可以被多個執行緒等待
- 其次執行緒可以喚醒條件變數。此時某個或所有等待此條件變數的執行緒都會被喚醒並繼續支援。
換句話說:使用條件變數可以讓許多執行緒一起等待某個時間的發生,當某個時間發生時,所有的執行緒可以一起恢復執行!
相信仔細的大家肯定在鎖的用法裡面見過NSCondition
,就是封裝了條件變數pthread_cond_t
和互斥鎖
- (void) signal {
pthread_cond_signal(&_condition);
}
// 其實這個函式是通過巨集來定義的,展開後就是這樣
- (void) lock {
int err = pthread_mutex_lock(&_mutex);
}
複製程式碼
NSConditionLock
藉助 NSCondition
來實現,它的本質就是一個生產者-消費者模型
。“條件被滿足”可以理解為生產者提供了新的內容。NSConditionLock
的內部持有一個NSCondition
物件,以及 _condition_value
屬性,在初始化時就會對這個屬性進行賦值:
// 簡化版程式碼
- (id) initWithCondition: (NSInteger)value {
if (nil != (self = [super init])) {
_condition = [NSCondition new]
_condition_value = value;
}
return self;
}
複製程式碼
臨界區
比互斥量更加嚴格的同步手段。在術語中,把臨界區的獲取稱為進入臨界區,而把鎖的釋放稱為離開臨界區。與互斥量和訊號量的區別:
- (1)互斥量和訊號量字系統的任何程式都是可見的。
- (2)臨界區的作用範圍僅限於本程式,其他程式無法獲取該鎖。
// 臨界區結構物件
CRITICAL_SECTION g_cs;
// 共享資源
char g_cArray[10];
UINT ThreadProc10(LPVOID pParam)
{
// 進入臨界區
EnterCriticalSection(&g_cs);
// 對共享資源進行寫入操作
for (int i = 0; i < 10; i++)
{
g_cArray[i] = a;
Sleep(1);
}
// 離開臨界區
LeaveCriticalSection(&g_cs);
return 0;
}
UINT ThreadProc11(LPVOID pParam)
{
// 進入臨界區
EnterCriticalSection(&g_cs);
// 對共享資源進行寫入操作
for (int i = 0; i < 10; i++)
{
g_cArray[10 - i - 1] = b;
Sleep(1);
}
// 離開臨界區
LeaveCriticalSection(&g_cs);
return 0;
}
……
void CSample08View::OnCriticalSection()
{
// 初始化臨界區
InitializeCriticalSection(&g_cs);
// 啟動執行緒
AfxBeginThread(ThreadProc10, NULL);
AfxBeginThread(ThreadProc11, NULL);
// 等待計算完畢
Sleep(300);
// 報告計算結果
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); //解鎖
複製程式碼
- 用條件變數實現讀寫鎖
這裡用條件變數+互斥鎖來實現。注意:條件變數必須和互斥鎖一起使用,等待、釋放的時候都需要加鎖。
#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); //喚醒其他因條件變數而產生的阻塞
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); //喚醒其他因條件變數而產生的阻塞
pthread_mutex_unlock(&mutex); //解鎖
複製程式碼
- 用互斥鎖實現讀寫鎖
這裡使用2個互斥鎖+1個整型變數來實現
#include <pthread.h> //多執行緒、互斥鎖所需標頭檔案
pthread_mutex_t r_mutex = PTHREAD_MUTEX_INITIALIZER; //定義和初始化互斥鎖
pthread_mutex_t w_mutex = PTHREAD_MUTEX_INITIALIZER;
int readers = 0; //記錄讀者的個數
寫模式:
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);
複製程式碼
- 用訊號量來實現讀寫鎖
這裡使用2個訊號量+1個整型變數來實現。令訊號量的初始數值為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);
複製程式碼
執行緒的安全是現在各個領域在多執行緒開發必須要掌握的基礎!只有對底層有所掌握,才能在真正的實際開發中遊刃有餘!現在的iOS開發乃至其他開發都是表面基礎層開發,真正大牛開發是必須要掌握的,這一篇部落格以供大家一起學習!