多線程、鎖和線程同步方案

多線程

多線程技術你們都很瞭解,並且在項目中也比較經常使用。好比開啓一個子線程來處理一些耗時的計算,而後返回主線程刷新UI等。首先咱們先簡單的梳理一下經常使用到的多線程方案。具體的用法這裏我就不說了,每一種方案你們能夠去查一下,網上教程不少。面試

常見的多線程方案

咱們比較經常使用的是GCD和NSOperation,固然還有NSThread,pthread。他們的具體區別咱們不詳細說,給出下面這一個表格,你們自行對比一下。數組

對比.png

容易混淆的術語

提到多線程,有一個術語是常常能聽到的,同步,異步,串行,併發安全

同步和異步的區別,就是是否有開啓新的線程的能力。異步具有開啓線程的能力,同步不具有開啓線程的能力。注意,異步只是具有開始新線程的能力,具體開啓與否還要跟隊列的屬性有關係。多線程

串行和併發,是指的任務的執行方式。併發是任務能夠多個同時執行,串行之能是一個執行完成後在執行下一個。併發

在面試的過程當中可能被問到什麼網狀況下會出現死鎖的問題,總結一下就是使用sync函數(同步)往當前的串行對列中添加任務時,會出現死鎖。異步

多線程的安全隱患

多線程和安全問題是分不開的,由於在使用多個線程訪問同一塊數據的時候,若是同時有讀寫操做,就可能產生數據安全問題。async

因此這時候咱們就用到了鎖這個東西。函數

其實使用鎖也是爲了在使用多線程的過程當中保障數據安全,除了鎖,而後一些其餘的實現線程同步來保證數據安全的方案,咱們一塊兒來了解一下。atom

線程同步方案

下面這些是咱們經常使用來實現線程同步方案的。spa

OSSpinLock
os_unfair_lock
pthread_mutex
NSLock
NSRecursiveLock
NSCondition
NSConditinLock
dispatch_semaphore
dispatch_queue(DISPATCH_QUEUE_SERIAL)
@synchronized

能夠看出來,實現線程同步的方案包括各類鎖,還有信號量,串行隊列。

咱們只挑其中不經常使用的來講一下使用方法。
下面是咱們模擬了存錢取錢的場景,下面是加鎖以前的代碼,運行以後確定是有數據問題的。

/**
 存錢、取錢演示
 */
- (void)moneyTest {
    self.money = 100;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self __saveMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self __drawMoney];
        }
    });
}

/**
 存錢
 */
- (void)__saveMoney {
    int oldMoney = self.money;
    sleep(.2);
    oldMoney += 50;
    self.money = oldMoney;
    
    NSLog(@"存50,還剩%d元 - %@", oldMoney, [NSThread currentThread]);
    
}

/**
 取錢
 */
- (void)__drawMoney {
    
    int oldMoney = self.money;
    sleep(.2);
    oldMoney -= 20;
    self.money = oldMoney;
    
    NSLog(@"取20,還剩%d元 - %@", oldMoney, [NSThread currentThread]);
    
}

加鎖的代碼,涉及到鎖的初始化、加鎖、解鎖這麼三部分。咱們從OSSpinLock開始說。

OSSpinLock自旋鎖

OSSpinLock叫作自旋鎖。那什麼叫自旋鎖呢?其實咱們能夠從大類上面把鎖分爲兩類,一類是自旋鎖,一類是互斥鎖。咱們經過一個例子來區分這兩類鎖。

若是線程A率先到達加鎖的部分,併成功加鎖,線程B到達的時候會由於已經被A加鎖而等待。若是是自旋鎖,線程B會經過執行一個循環來實現等待,咱們不用管它循環執行了什麼,只要知道他在那"轉圈圈"等着就行。若是是互斥鎖,那線程B在等待的時候會休眠。

使用OSSpinLock須要導入頭文件#import <libkern/OSAtomic.h>

//聲明一個鎖
@property (nonatomic, assign) OSSpinLock lock;

// 鎖的初始化
self.lock = OS_SPINLOCK_INIT;

在咱們這個例子中,存錢取錢都是訪問了money,因此咱們要在存和取的操做中使用同一個鎖。

/**
 存錢
 */
- (void)__saveMoney {
    OSSpinLockLock(&_lock);
    
    //....省去中間的邏輯代碼
    
    OSSpinLockUnlock(&_lock);
}

/**
 取錢
 */
- (void)__drawMoney {
    OSSpinLockLock(&_lock);
    
    //....省去中間的邏輯代碼
    
    OSSpinLockUnlock(&_lock);
}

這就是簡單的自旋鎖的使用,咱們發如今使用的過程當中,Xcode一直提醒咱們這個OSSpinLock被廢棄了,讓咱們使用os_unfair_lock代替。OSSpinLock之因此會被廢棄是由於它可能會產生一個優先級反轉的問題。

具體來講,若是一個低優先級的線程得到了鎖並訪問共享資源,那高優先級的線程只能忙等,從而佔用大量的CPU。低優先級的線程沒法和高優先級的線程競爭(CPU會給高優先級的線程分配更多的時間片),因此會致使低優先級的線程的任務一直完不成,從而沒法釋放鎖。

os_unfair_lock的用法跟OSSpinLock很像,就不單獨說了。

pthread_mutex
Default

一看到這個pthread咱們應該就能知道這是一種跨平臺的方案了。首先仍是來看用法。

//聲明一個鎖
@property (nonatomic, assign) pthread_mutex_t lock;

//初始化
pthread_mutex_init(pthread_mutex_t *restrict _Nonnull, const pthread_mutexattr_t *restrict _Nullable)

咱們能夠看到在初始化鎖的時候,第一個參數是鎖的地址,第二個參數是一個pthread_mutexattr_t類型的地址,若是咱們不傳pthread_mutexattr_t,直接傳一個NULL,至關於建立一個默認的互斥鎖。

//方式一
pthread_mutex_init(mutex, NULL);
//方式二
// - 建立attr
pthread_mutexattr_t attr;
// - 初始化attr
pthread_mutexattr_init(&attr);
// - 設置attr類型
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_DEFAULT);
// - 使用attr初始化鎖
pthread_mutex_init(&_lock, &attr);
// - 銷燬attr
pthread_mutexattr_destroy(&attr);

上面兩個方式是一個效果,那爲何使用attr,那就說明除了default類型的還有其餘類型,咱們後面再說。

在使用的時候用pthread_mutex_lock(&_lock);pthread_mutex_unlock(&_lock);加鎖解鎖。

NSLock就是對這種普通互斥鎖的OC層面的封裝。

RECURSIVE 遞歸鎖

調用pthread_mutexattr_settype的時候若是類型傳入PTHREAD_MUTEX_RECURSIVE,會建立一個遞歸鎖。舉個例子吧。

// 僞代碼
-(void)test {
    lock;
    [self test];
    unlock;
}

若是是普通的鎖,當咱們在test方法中,遞歸調用test,應該會出現死鎖,由於被lock,在遞歸調用時沒法調用,一直等待。可是若是鎖是遞歸鎖,他會容許同一個線程屢次加鎖和解鎖,就能夠解決這個問題了。

NSRecursiveLock是對遞歸鎖的封裝。

Condition 條件鎖

咱們直接上這種鎖的使用方法,

- (void)otherTest
{
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 線程1
// 刪除數組中的元素
- (void)__remove {
    pthread_mutex_lock(&_mutex);
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) {
        // 等待
        pthread_cond_wait(&_cond, &_mutex);
    }
    [self.data removeLastObject];
    NSLog(@"刪除了元素");
    
    pthread_mutex_unlock(&_mutex);
}

// 線程2
// 往數組中添加元素
- (void)__add {
    pthread_mutex_lock(&_mutex);
    
    sleep(1); 
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    
    // 信號
    pthread_cond_signal(&_cond);
    // 廣播
//    pthread_cond_broadcast(&_cond);
    
    pthread_mutex_unlock(&_mutex);
}

咱們建立了兩個線程,一個往數組中添加數據,一個刪除數據,咱們經過這個條件鎖實現的效果就是在數組中尚未數據的時候等待,數組中添加了一個數據以後在進行刪除。

條件鎖就是互斥鎖+條件。咱們聲明一個條件並初始化。

@property (assign, nonatomic) pthread_cond_t cond;
//使用完後也要pthread_cond_destroy(&_cond);
pthread_cond_init(&_cond, NULL);

__remove方法中

if (self.data.count == 0) {
    // 等待
    pthread_cond_wait(&_cond, &_mutex);
}

若是線程1率先拿到所並加鎖,執行到上面代碼這裏發現數組中尚未數據,就執行pthread_cond_wait,此時線程1會暫時放開_mutex這個鎖,並在這休眠等待。

線程2在__add方法中最開始由於拿不到鎖,因此等待,在線程1休眠放開鎖以後拿到鎖,加鎖,並執行爲數組添加數據的代碼。添加完了以後會發個信號通知等待條件的線程,並解鎖。

pthread_cond_signal(&_cond);
    
    pthread_mutex_unlock(&_mutex);

線程2執行了pthread_cond_signal以後,線程1就收到了通知,退出休眠狀態,繼續執行下面的代碼。

這個地方可能有人會有疑問,是否是線程2應該先unlock再cond_dingnal,其實這個地方順序沒有太大差異,由於線程2執行了pthread_cond_signal以後,會繼續執行unlock代碼,線程1收到signal通知後會推出休眠狀態,同時線程1須要再一次持有這個鎖,就算此時線程2尚未unlock,線程1等到線程2 unlock 的時間間隔很短,等到線程2 unlock 後線程1會再去持有這個鎖,並加鎖。

NSCondition就是OC層面的條件鎖,內部把mutex互斥鎖和條件封裝到了一塊兒。NSConditionLock其實也差很少,NSConditionLock能夠指定具體的條件,這兩個OC層面的類的用法你們能夠自行上網搜索。

dispatch_semaphore 信號量
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
//初始化
self.semaphore = dispatch_semaphore_create(5);

在初始化一個信號的的過程當中傳入dispatch_semaphore_create的值,其實就表明了容許幾個線程同時訪問。

再回到以前咱們存錢取錢這個例子。

self.moneySemaphore = dispatch_semaphore_create(1);

咱們一次只容許一個線程訪問,因此在初始化的時候傳1。下面就是使用方法。

- (void)__drawMoney
{
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
    
    // ... 省略代碼
    
    dispatch_semaphore_signal(self.moneySemaphore);
}

- (void)__saveMoney
{
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
    
    // ... 省略代碼
    
    dispatch_semaphore_signal(self.moneySemaphore);
}

dispatch_semaphore_wait是怎麼上鎖的呢?
若是信號量>0的時候,讓信號量-1,並繼續往下執行。
若是信號量<=0的時候,休眠等待。
就這麼簡單。

dispatch_semaphore_signal讓信號量+1。

小提示
在咱們平時使用這種方法的時候,能夠把信號量的代碼提取出來定義一個宏。

#define SemaphoreBegin \
static dispatch_semaphore_t semaphore; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
    semaphore = dispatch_semaphore_create(1); \
}); \
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

#define SemaphoreEnd \
dispatch_semaphore_signal(semaphore);

讀寫安全方案

上面咱們講到的線程同步方案都是每次只容許一個線程訪問,在實際的狀況中,讀寫的同步方案應該下面這樣:

  1. 每次只能有一個線程寫
  2. 能夠有多個線程同時讀
  3. 讀和寫不能同時進行

這就是多讀單寫,用於文件讀寫的操做。在咱們的iOS中能夠用下面這兩種解決方案。

pthread_rwlock 讀寫鎖

這個讀寫鎖的用法很簡單,跟以前的普通互斥鎖都差很少,你們隨便搜一下應該就能搜到,我就不拿出來寫了,這裏主要是提一下這種鎖,你們之後有須要的時候能夠用。

dispatch_barrier_async 異步柵欄

首先在使用這個函數的時候,咱們要用本身建立的併發隊列。
若是傳入的是一個串行隊列或者全局的併發隊列,那dispatch_barrier_async等同於dispatch_async的效果。

self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(self.queue, ^{
    [self read];
});
        
dispatch_barrier_async(self.queue, ^{
    [self write];
});

在讀取數據的時候,使用dispatch_async往對列中添加任務,在寫數據時,用dispatch_barrier_async添加任務。

dispatch_barrier_async添加的任務會等前面全部的任務都執行完,他再執行,並且他執行的時候,不容許有別的任務同時執行。

atomic

咱們都知道這個atomic是原子性的意思。他保證了屬性setter和getter的原子性操做,至關於在set和get方法內部加鎖。

atomic修飾的屬性是讀/寫安全的,但不是線程安全。

假設有一個 atomic 的屬性 "name",若是線程 A 調用 [self setName:@"A"],線程 B 調用 [self setName:@"B"],線程 C 調用 [self name],那麼全部這些不一樣線程上的操做都將依次順序執行——也就是說,若是一個線程正在執行 getter/setter,其餘線程就得等待。所以,屬性 name 是讀/寫安全的。

可是,若是有另外一個線程 D 同時在調[name release],那可能就會crash,由於 release 不受 getter/setter 操做的限制。也就是說,這個屬性只能說是讀/寫安全的,但並非線程安全的,由於別的線程還能進行讀寫以外的其餘操做。線程安全須要開發者本身來保證。

相關文章
相關標籤/搜索