ObjC 多線程簡析(一)-多線程簡述和線程鎖的基本應用

在iOS開發中,常常會遇到將耗時操做放在子線程中執行的狀況。api

通常狀況下咱們會使用NSThread、NSOperation和GCD來實現多線程的相關操做。初次以外pthread也能夠用於多線程的相關開發。數組

pthread提供了一套C語言的api,它是跨平臺的,須要開發人員自行管理線程的生命週期;NSThread提供了一套OC的api,使用更加簡單,可是線程的生命週期也是須要開發人員本身管理的;GCD也提供了C語言的api,它充分利用了CPU多核處理事件的能力,而且能夠本身管理線程的生命週期;NSOperation是對GCD作了一層OC的封裝,更加面向對象,生命週期也由其自動管理。安全

本篇主要使用GCD來介紹iOS開發中的多線程狀況,以及實現線程同步的些許方式。bash

GCD的基本使用

基本概念

GCD提供了同步和異步處理事情的能力,分別調用dispatch_sync(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)dispatch_async(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)建立任務。同步只能在當前線程中執行,並不會建立一個新的線程。只有異步纔會建立新的線程,任務也是在新的線程中執行的。多線程

GCD實現多線程一般須要依賴一個隊列,而GCD提供了串行和並行隊列。串行隊列是指任務一個接着一個執行,下一個任務的執行必須等待上一個任務執行結束。並行隊列則能夠同時執行多個任務,可是併發任務的執行須要依賴於異步函數(dispatch_async)。併發

任務和隊列

GCD的多線程技術須要往函數中添加一個隊列,那麼這四種狀況排列組合將會出現什麼狀況呢?可使用下表進行表示:app

GCD任務和隊列

當使用dispatch_sync的時候不管是併發隊列仍是串行隊列或者主線程,全都不會開啓新的線程,而且都是串行執行任務。異步

當使用dispatch_async的時候,除了在主線程的狀況下,全都會開啓新的線程,而且只有在併發隊列的時候纔會並行執行任務。async

隊列組

GCD提供了隊列組的api,能夠實如今一個隊列組中控制隊列中任務的執行順序:函數

dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{
        NSLog(@"任務1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"任務2");
    });
    dispatch_group_notify(group, queue, ^{
        NSLog(@"任務3");
    });
複製代碼

線程同步

儘管多線程提供了能充分利用線程處理事情的能力,好比多任務下載、處理耗時操做等。可是當多條線程操做同一塊資源的時候就可能會出現不合理的現象(數據錯亂,數據安全),這是由於多線程執行的順序和時間是不肯定的。因此當一條線程拿到資源進行操做的時候,下一條線程拿到可能仍是以前的資源。因此線程同步就是讓多線程在同一時間只有一條線程在操做資源。

在實現線程同步的時候,咱們首先想到的應該是給資源操做任務加鎖。那麼ObjC中提供了哪些線程鎖呢?

OSSpinLock

OSSpinLock是一種自旋鎖。自旋鎖在加鎖狀態下,等待鎖的線程會處於忙等的狀態,一直佔用着CPU的資源。OSSpinLock目前已經再也不安全,api中也再也不建議使用它,由於它可能出現優先級反轉的問題。

優先級反轉的問題就是,當優先級比較高的線程在等待鎖,它須要繼續往下執行,因此優先級低的佔用着鎖的線程就無法將鎖釋放。

OSSpinLock存在於libkern/OAtomic.h中,經過定義咱們能夠看出它是一個int32_t類型的(定義:typedef int32_t OSSpinLock;)。使用OSSpinLock的時候須要對鎖進行初始化,而後再操做數據以前進行加鎖,操做數據以後進行解鎖。

// 初始化OSSpinLock
_osspinlock = OS_SPINLOCK_INIT;

// 加鎖
OSSpinLockLock(&_osspinlock);

// 操做數據
// ...

// 解鎖
OSSpinLockUnlock(&_osspinlock);
複製代碼

os_unfair_lock

iOS10以後apple廢棄了OSSpinLock使用os/lock中定義的os_unfair_lock。經過彙編來看os_unfair_lock並非一種自旋鎖,在加鎖狀態下,等待鎖的線程會處於休眠狀態,不佔用CPU資源。

一樣使用os_unfair_lock的時候也須要初始化。

// 初始化os_unfair_lock
_osunfairLock = OS_UNFAIR_LOCK_INIT;

// 加鎖
os_unfair_lock_lock(&(_osunfairLock));

// 操做數據
// ...

// 解鎖
os_unfair_lock_unlock(&(_osunfairLock));
複製代碼

pthread_mutex

pthread_mutex是屬於pthreadapi中的,mutex屬於互斥鎖。在加鎖狀態下,等待鎖的線程會處於休眠狀態,不會佔用CPU的資源。

mutex初始化的時候須要傳入一個鎖的屬性(int pthread_mutex_init(pthread_mutex_t * __restrict,const pthread_mutexattr_t * _Nullable __restrict);),若是傳NULL就是默認狀態PTHREAD_MUTEX_DEFAULT也就是PTHREAD_MUTEX_NORMAL

pthread_mutex狀態pthread_mutexattr_t的定義:

/* * Mutex type attributes */
#define PTHREAD_MUTEX_NORMAL 0
#define PTHREAD_MUTEX_ERRORCHECK 1
#define PTHREAD_MUTEX_RECURSIVE 2
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
複製代碼

初始化mutex以後再須要加鎖的時候調用pthread_mutex_lock(),解鎖的時候調用pthread_mutex_unlock();

另外pthread_mutex中有一個銷燬鎖的方法int pthread_mutex_destroy(pthread_mutex_t *);在不須要鎖的時候一般須要調用一下,將mutex鎖的地址做爲參數傳入。

pthread_mutex 遞歸鎖

在開發中時常遇到遞歸調用的狀況,若是在一個函數中進行了加鎖和解鎖操做,而後在解鎖以前遞歸。那麼遞歸的時候線程會發現已經加鎖了,會一直在等待鎖被釋放。這樣遞歸就無法繼續往下進行,鎖也永遠不會被釋放,就形成了死鎖的現象。

爲了解決這個問題,pthread_mutex的屬性中提供了將pthread_mutex變爲遞歸鎖的屬性。’

遞歸鎖就是同一條線程能夠對一把鎖進行重複加鎖,而不一樣線程卻不能夠。這樣每一次遞歸都會加一次鎖,因此互不衝突,當遞歸結束以後會從後往前以此解鎖。不一樣線程的時候,遞歸鎖會判斷這條線程正在等待的鎖與加鎖的不是一條線程,因此不會進行加鎖,而是在等待鎖被釋放。

建立遞歸鎖的時候須要初始化一個pthread_mutexattr_t屬性:

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_pthreadMutex, &attr);
pthread_mutexattr_destroy(&attr);
複製代碼

屬性使用完也須要進行銷燬,調用pthread_mutexattr_destroy函數實現。

pthread_mutex 條件

當兩條線程在操做同一資源,可是一條線程的執行,須要依賴另外一條線程的執行結果的時候,因爲默認多線程的訪問時間和順序是不固定的,因此不容易實現。pthread_mutex提供了執行條件的額api,使用pthread_cond_init()初始化一個條件。

在須要等待的地方使用pthread_cond_wait();等待信號的到來,此時線程會進入休眠狀態而且放開mutex鎖,等待信號到來的時候會被喚醒而且對mutex加鎖。信號發送使用pthread_cond_signal()來告訴等待的線程,本身的線程處理完了,依賴的能夠開始執行了,等待的線程就會往下繼續執行。也可使用pthread_cond_broadcast()進行廣播,告訴全部等待的該條件的線程。條件也是須要銷燬的,使用pthread_cond_destroy()銷燬條件。

好比兩條線程操做一個數組,a線程負責刪除數組,b線程負責往數組中添加元素。a線程刪除元素的條件是數組中必須有元素存在。

代碼以下:

#import "ViewController.h"
#import <pthread.h>

@interface ViewController ()

@property (nonatomic, assign) pthread_mutex_t pthreadMutex;
@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, assign) pthread_cond_t cond;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    pthread_mutex_init(&_pthreadMutex, NULL);
    pthread_cond_init(&_cond, NULL);
    [self testCond];
}

- (void)testCond {
    [[[NSThread alloc] initWithTarget:self selector:@selector(removeObject) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(addObject) object:nil] start];
}

- (void)addObject {
    pthread_mutex_lock(&_pthreadMutex);
    NSLog(@"end start %@", _array);
    [self.array addObject:@"test"];
    NSLog(@"end add %@", _array);
    pthread_cond_signal(&_cond);
    pthread_mutex_unlock(&_pthreadMutex);
}

- (void)removeObject {
    pthread_mutex_lock(&_pthreadMutex);
    pthread_cond_wait(&_cond, &_pthreadMutex);
    NSLog(@"start remove %@", _array);
    [self.array removeLastObject];
    NSLog(@"end remove %@", _array);
    pthread_mutex_unlock(&_pthreadMutex);
}

- (void)dealloc {
    pthread_cond_destroy(&_cond);
    pthread_mutex_destroy(&_pthreadMutex);
}
複製代碼

NSLock和NSRecursiveLock

NSLock和NSRecursiveLock是對pthread_mutex普通鎖和遞歸鎖的OC封裝。更加面向對象,使用也比較簡單。它使用了NSLocking協議來生命加鎖和解鎖的方法。因爲上面已經對pthread_mutex進行了簡單的介紹,NSLock和NSRecursiveLock的api都是OC的也比較簡單。這裏再也不贅述,只是說明有這樣一種實現線程同步的方法。

NSCondition和NSConditionLock

NSCondition是對mutexcond的封裝,因爲NSCondition也遵循了NSLocking協議,因此他也能夠加鎖和加鎖。使用效果和pthread的cond同樣,在等待的時候調用wait,發送信號調用singal

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, strong) NSCondition *cond;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.array = [NSMutableArray array];
    self.cond = [[NSCondition alloc] init];
    [self testCond];
}

- (void)testCond {
    [[[NSThread alloc] initWithTarget:self selector:@selector(removeObject) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(addObject) object:nil] start];
}

- (void)addObject {
    [_cond1 lock];
    NSLog(@"end start %@", _array);
    [self.array addObject:@"test"];
    NSLog(@"end add %@", _array);
    [_cond1 signal];
    [_cond unlock];
}

- (void)removeObject {
    [_cond lock];
    [_cond wait];
    NSLog(@"start remove %@", _array);
    [self.array removeLastObject];
    NSLog(@"end remove %@", _array);
    [_cond unlock];
}
複製代碼

NSConditionLock是對NSCondition的又一層封裝。NSConditionLock能夠添加條件,經過- (instancetype)initWithCondition:(NSInteger)condition;初始化並添加一個條件,條件是NSInteger類型的。解鎖的時候是按照這個條件進行解鎖的。依然是上述例子:

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, strong) NSConditionLock *lock2;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.array = [NSMutableArray array];
    self.lock2 = [[NSConditionLock alloc] initWithCondition:1];
    [self testCond];
}

- (void)testCond {
    [[[NSThread alloc] initWithTarget:self selector:@selector(removeObject) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(addObject) object:nil] start];
}


- (void)addObject {
    [_lock2 lock];
    NSLog(@"end start %@", _array);
    [self.array addObject:@"test"];
    NSLog(@"end add %@", _array);
    [_lock2 unlockWithCondition:2];
}

- (void)removeObject {
    [_lock2 lockWhenCondition:2];
    NSLog(@"start remove %@", _array);
    [self.array removeLastObject];
    [_lock2 unlock];
}
複製代碼

dispatch_semaphore_t

GCD提供了一個信號量的方式也能夠解決線程同步的問題。

使用dispatch_semaphore_create();建立一個信號量,使用dispatch_semaphore_wait()等待信號的到來,使用dispatch_semaphore_signal()發送一個信號。

dispatch_semaphore_wait()會根據第二個參數dispatch_time_t timeout判斷超時時間,通常咱們會設置爲DISPATCH_TIME_FOREVER一直等待信號的到來。若是此時信號量的值大於0,那麼就讓信號量的值減1,而後繼續往下執行代碼,而若是信號量的值小於等於0,那麼就會休眠等待,直到信號量的值變成大於0,再就讓信號量的值減1,而後繼續往下執行代碼。dispatch_semaphore_signal()發送一個信號,而且讓信號量加1。

經典的買票例子:

#import "ViewController.h"

@interface ViewController ()
@property (strong, nonatomic) dispatch_semaphore_t ticketSemaphore;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.ticketSemaphore = dispatch_semaphore_create(1);
    [self ticket];
}

- (void)saleTicket {
    dispatch_semaphore_wait(_ticketSemaphore, DISPATCH_TIME_FOREVER);
    int oldTicketCount = _ticketCount;
    sleep(.2);
    oldTicketCount --;
    _ticketCount = oldTicketCount;
    NSLog(@"剩餘的票數%d",_ticketCount);
    dispatch_semaphore_signal(_ticketSemaphore);
}

- (void)ticket {
    self.ticketCount = 20;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

複製代碼

串行隊列

使用GCD的串行隊列實現線程同步,原理是由於串行隊列必須一個接着一個執行,只有在執行完上一個任務的狀況下,下一個任務纔會繼續執行。

使用dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);建立一條串行隊列,將多線程任務都放到這條串行隊列當中執行。

#import "ViewController.h"

@interface ViewController ()
@property (strong, nonatomic) dispatch_semaphore_t ticketSemaphore;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.ticketSemaphore = dispatch_semaphore_create(1);
    [self ticket];
}

- (void)saleTicket {
    dispatch_semaphore_wait(_ticketSemaphore, DISPATCH_TIME_FOREVER);
    int oldTicketCount = _ticketCount;
    sleep(.2);
    oldTicketCount --;
    _ticketCount = oldTicketCount;
    NSLog(@"剩餘的票數%d",_ticketCount);
    dispatch_semaphore_signal(_ticketSemaphore);
}

- (void)ticket {
    self.ticketCount = 20;
    
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}
複製代碼

@synchronized

@synchronized是對mutex遞歸鎖的封裝。須要傳遞一個obj,@synchronized(obj)內部會生成obj對應的遞歸鎖,而後進行加鎖、解鎖操做。

使用:

// 建立一個初始化一次的obj
- (NSObject *)lock {
    static NSObject *lock;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        lock = [[NSObject alloc] init];
    });
    return lock;
}

// 加鎖買票經典案例
- (void)saleTicket {
    @synchronized([self lock]) {
        int oldTicketCount = _ticketCount;
        sleep(.2);
        oldTicketCount --;
        _ticketCount = oldTicketCount;
        NSLog(@"剩餘的票數%d",_ticketCount);
    }
}

- (void)ticket {
    self.ticketCount = 20;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

// 遞歸鎖
- (void)test {
    @synchronized ([self lock]) {
        NSLog(@"%s",__func__);
        [self test];
    }
}
複製代碼

atomic

在OC中定義屬性一般會指定屬性的原子性也就是使用nonatomic關鍵字定義非原子性的屬性,而其默認爲atomic原子性

atomic用於保證屬性setter、getter的原子性操做,至關於在getter和setter內部加了線程同步的鎖,可是它並不能保證使用屬性的過程是線程安全的。

讀寫安全

當咱們多項城操做一個文件的時候,若是同時進行讀寫的話,會形成讀的內容不徹底等問題。因此咱們常常會在多線程讀寫文件的時候,實現多讀單寫的方案。即在同一時間能夠有多條線程在讀取文件內容,可是隻能有一條線程執行寫文件的操做。

下面經過模擬對文件的讀寫操做而且經過pthread_rwlock_tdispatch_barrier_async來實現文件讀寫的線程安全。

pthread_rwlock_t

使用pthread_rwlock_t的時候,須要調用pthread_rwlock_init()進行初始化。而後在讀的時候調用pthread_rwlock_rdlock()對讀操做進行加鎖。在寫的時候調用pthread_rwlock_wrlock()對讀進行加鎖。使用pthread_rwlock_unlock()進行解鎖。在用不到鎖的時候使用pthread_rwlock_destroy()對鎖進行銷燬。

#import "SecondViewController.h"
#import <pthread.h>

@interface SecondViewController ()

@property (nonatomic, assign) pthread_rwlock_t lock;

@end

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    pthread_rwlock_init(&_lock, NULL);
    
    for (int i = 0; i < 10; i++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(read) object:nil] start];
        [[[NSThread alloc] initWithTarget:self selector:@selector(write) object:nil] start];
    }

}

- (void)read {
    
    pthread_rwlock_rdlock(&_lock);
    sleep(1);
    NSLog(@"%s",__func__);
    pthread_rwlock_unlock(&_lock);
    
}

- (void)write {
    
    pthread_rwlock_wrlock(&_lock);
    sleep(1);
    NSLog(@"%s",__func__);
    pthread_rwlock_unlock(&_lock);
    
}

- (void)dealloc {
    pthread_rwlock_destroy(&_lock);
}
複製代碼

經過打印結果的時間咱們能夠發現,沒有同一時間執行讀寫的操做,只有同一時間讀,這樣就保證了讀寫的線程安全。

打印結果以下:

打印結果

dispatch_barrier_async

GCD提供了一個異步柵欄函數,這個函數要求傳入的併發隊列必須是本身經過dispatch_queue_cretate建立的。

它的原理就是當執行到dispatch_barrier_async的時候就至關於建立了一個柵欄將線程的讀寫操做隔離開,這個時候只能有一個線程來執行dispatch_barrier_async裏面的任務。

當咱們使用它來處理讀寫安全的操做的時候,使用dispatch_barrier_async來隔離寫的操做,就能保證同一時間只能有一條線程對文件執行寫的操做。

代碼以下:

#import "SecondViewController.h"

@interface SecondViewController ()

@end

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 5; i++) {
        dispatch_async(queue, ^{
            [self read];
        });
        dispatch_async(queue, ^{
            [self read];
        });
        dispatch_async(queue, ^{
            [self read];
        });
        dispatch_barrier_async(queue, ^{
            [self write];
        });
    }
}

- (void)read {
    sleep(1);
    NSLog(@"%s",__func__);
}

- (void)write {
    sleep(1);
    NSLog(@"%s",__func__);
}
複製代碼

依然經過打印結果的時間分析是否實現了文件的讀寫安全。下面是打印結果,明顯看出文件的讀寫是安全的。

打印結果:

打印結果

總結

本篇主要介紹了ObjC和iOS開發中經常使用的多線程方案,並經過賣票的經典案例介紹了多線程操做統一資源形成的隱患以及經過線程同步方案解決隱患的幾種方法。另外還介紹了文件讀寫鎖以及GCD提供的柵欄異步函數處理多線程文件讀寫安全的兩種用法。

相關文章
相關標籤/搜索