多執行緒安全-iOS開發注意咯!!!

Cooci_和諧學習_不急不躁發表於2019-05-06

多執行緒,作為實現軟體併發執行的一個重要的方法,也開始具有越來越重要的地位!

多執行緒安全-iOS開發注意咯!!!

正式因為多執行緒能夠在時間片裡被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開發乃至其他開發都是表面基礎層開發,真正大牛開發是必須要掌握的,這一篇部落格以供大家一起學習!

相關文章