多線程安全-iOS開發注意咯!!!

多線程,做爲實現軟件併發執行的一個重要的方法,也開始具備愈來愈重要的地位!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

儘管原子操做很是的簡單,可是它只適合於比較簡單特定的場合。在複雜的場合下,好比咱們要保證一個複雜的數據結構更改的原子性,原子操做指令就力不從心了,async

若是臨界區的執行時間過長,使用自旋鎖不是個好主意。以前咱們介紹過期間片輪轉算法,線程在多種狀況下會退出本身的時間片。其中一種是用完了時間片的時間,被操做系統強制搶佔。除此之外,當線程進行 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開發乃至其餘開發都是表面基礎層開發,真正大牛開發之路還請繼續努力,這一篇博客以供你們一塊兒學習!

微信公衆號「iOSSir」!每日干貨、程序資訊分享!
看你想看的,得你想得的!
iOS開發者微信交流羣!若是二維碼過時了,能夠添加公衆號小助理微信「kele22558」拉你進羣!

相關文章
相關標籤/搜索