Objective-C基礎之九(深刻理解多線程)

什麼是線程、多線程?

在學習iOS多線程應用以前,咱們先來學習一下什麼是線程?php

  • 線程是操做系統可以進行運算調度的最小單位,它被包含在進程之中,是進程的實際運做單位,一條線程指的是進程中一個單一順序的控制流。
  • 系統中正在運行的每個應用程序都是一個進程,系統會爲每一個進程分配獨立的內存空間。而一個進程中的全部任務都是在線程中執行的,所以每一個進程至少得有一個線程,這也就是咱們日常所說的主線程。
  • 一個進程能夠開啓多條線程,多條線程並行執行不一樣的任務,這就是多線程
  • 說到多線程,就不得不提CPUCPU在任意時刻只能執行一條機器指令。而線程只有獲取到CPU的使用權才能執行指令。
  • 多線程併發運行,實際上是CPU(單核)快速在多條線程之間調度,因爲調度線程的時間足夠快,因此就形成了多線程併發執行的假象。調度線程的時間其實就是CPU分配給每一個線程能夠運行的一段時間,稱爲時間片。同時爲了提升CPU的執行效率,系統採用了時間片輪轉調度算法來進行線程調度。

以上線程調度說的是單核設備,多核設備能夠經過並行來同時執行多個線程ios

iOS中常見的多線程方案

在iOS中有四種多線程方案,對好比下算法

方案 簡介 語言 線程生命週期 使用頻率
pthread 一套通用的多線程API
適用於Unix\Linux\Windows等系統
跨平臺\可移植
使用難度大
C 開發者手動管理 幾乎不用
NSThread 底層是pthread
使用更加面向對象
使用方便,能夠執行操做線程對象
OC 開發者手動管理 偶爾使用
GCD 替代NSThread
充分利用設備的多核
C 自動管理 經常使用
NSOperation 對GCD的封裝
使用更加面向對象
增長了一些使用功能
OC 自動管理 經常使用

pthread

pthread是基於c語言的一套多線程API,正是由於底層是C語言,因此pthread可以在不一樣的操做系統上使用,移植性很強。可是pthread使用起來特別麻煩,並且須要手動管理線程的聲明週期,所以基本不多使用,此處也不作過多介紹。swift

NSThread

NSThread是蘋果官方提供的一套操做線程的API,它是面向對象的,而且是輕量級的,使用靈活。可是和pthread同樣,NSThread也須要開發者手動管理線程的生命週期。所以也不多使用,可是NSThread提供了一些很是實用的方法數組

#pragma mark - 線程建立
//獲取當前線程
 +(NSThread *)currentThread; 
//建立線程後自動啓動線程
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
//線程休眠,可設置休眠結束時間
+ (void)sleepUntilDate:(NSDate *)date;
//線程休眠多久
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
//取消線程
- (void)cancel;
//啓動線程
- (void)start;
//退出線程
+ (void)exit;
// 得到主線程
+ (NSThread *)mainThread;
//初始化方法
- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument NS_AVAILABLE(10_5, 2_0);
//是否正在執行
- (BOOL)isExecuting NS_AVAILABLE(10_5, 2_0);
//是否執行完成
- (BOOL)isFinished NS_AVAILABLE(10_5, 2_0);
//是否取消線程
- (BOOL)isCancelled NS_AVAILABLE(10_5, 2_0);
- (void)cancel NS_AVAILABLE(10_5, 2_0);
//線程啓動
- (void)start NS_AVAILABLE(10_5, 2_0);


#pragma mark - 線程通訊
//與主線程通訊
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
  // equivalent to the first method with kCFRunLoopCommonModes
//與其餘子線程通訊
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
  // equivalent to the first method with kCFRunLoopCommonModes
//隱式建立並啓動線程
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg NS_AVAILABLE(10_5, 2_0);
複製代碼

NSThread的用法也很是簡單,這裏不作介紹,有興趣的同窗能夠根據系統提供的API去進行嘗試。安全

NSThread在日常開發中也有使用,例如咱們常用[NSThread currentThread]來獲取當前線程,使用[NSThread mainThread]來獲取主線程。線程保活也是基於NSThread和RunLoop來實現的。bash

GCD(重點介紹)

GCD是蘋果爲解決多核設備並行運算而提出的解決方案,它會合理的利用CPU多核的特性。而且GCD可以自動管理線程的生命週期(好比建立線程、任務調度、銷燬線程等等),咱們只須要告訴GCD具體要執行的任務,不須要編寫任何關於線程的代碼。同時GCD結合block使用更加簡潔,所以在多線程開發中,GCD是首選。網絡

任務和隊列

在學習GCD以前,首先來學習兩個比較重要的概念:任務隊列多線程

任務

任務其實就是咱們須要執行的操做,在GCD中,咱們一般將須要執行的操做放在block中。執行任務有兩種方式:同步異步併發

  • 同步:同步表示任務調用一旦開始,那麼調用者必須等到任務返回以後,才能進行後續操做。同步任務是在當前線程中執行,不會開闢新的線程。
  • 異步:異步則表示任務一調用就會當即返回,不會阻礙調用者執行下一步操做。而任務實際是在新開闢的線程中執行。

所以,同步和異步最大的區別就是:是否具備開闢新線程的能力。

隊列

在GCD中,隊列主要分爲兩種:串行隊列併發隊列

  • 串行隊列:表示同一時間只會有一個任務執行,執行完畢後纔會執行下一個任務。串行隊列只會開啓一個線程執行任務。
  • 併發隊列:表示同一時間有多個任務在執行。這也就意味着併發隊列能夠開啓多個線程同時執行任務。

串行隊列併發隊列任務的插入方式都遵循FIFO(先進先出)原則,也就是新的任務總會插入到隊列的末尾,可是串行隊列中先進入隊列的任務會先執行,而且等到任務執行完以後纔會執行後面的任務。而併發隊列則會同時執行隊列中的多個任務,而且任務之間不會相互等待,任務的執行順序和執行過程也不可預測。

GCD用法

GCD的使用步驟其實很簡單,主要分爲兩個步驟

  • 建立隊列
  • 向隊列中添加任務(同步任務或異步任務)

建立隊列

GCD中的隊列有兩種,串行隊列併發隊列,除此以外,GCD還提供了兩種特殊的隊列,一種是主隊列(其實就是一個串行隊列),一種是全局隊列(併發隊列)。

建立隊列是經過dispatch_queue_create函數,它有兩個參數:

  • 第一個參數是隊列的惟一標識,爲char *類型,自定義的隊列建議使用全局惟一的標識,防止衝突
  • 第二個參數是隊列的類型,DISPATCH_QUEUE_SERIAL表示建立串行隊列,DISPATCH_QUEUE_CONCURRENT表示建立併發隊列。

建立隊列的代碼以下:

//建立串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
//建立併發隊列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//獲取全局併發隊列(參數1:隊列優先級  參數二:保留字段,通常傳0)
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//獲取主隊列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
複製代碼

這裏須要注意的是:主隊列其實就是一個普通的串行隊列,任何添加到主隊列的任務都會在主線程中執行

同步、異步添加任務

GCD中,添加任務的方式也有兩種,使用dispatch_sync建立同步任務和使用dispatch_async建立異步任務。無論是建立同步任務仍是異步任務,都須要指定隊列dispatch_queue_t

  • 在串行隊列中添加同步和異步任務
//建立串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"任務1");
dispatch_async(serialQueue, ^{
    sleep(3);
    NSLog(@"任務2--%@",[NSThread currentThread]);
});
NSLog(@"任務3");
dispatch_sync(serialQueue, ^{
    sleep(1);
    NSLog(@"任務4--%@",[NSThread currentThread]);
});
NSLog(@"任務5");
複製代碼

最終輸出的結果以下:

任務1和任務3先打印,以後纔會打印任務2。執行完任務2以後,纔會執行任務4,而且執行完任務4,最後纔會執行任務5。由此就能夠驗證上文中的結論:

  1. 異步任務不會阻塞當前線程,而且異步任務是在新開闢的線程中執行。(任務1和任務3先執行,任務2後執行)
  2. 同步任務會阻塞當前線程,只有執行完同步任務以後纔會執行後面的任務(執行完任務4纔會執行任務5)。
  3. 串行隊列中的任務遵循FIFO(先進先出)原則,先添加進去的任務先執行,而且前面的任務執行完成以後纔會執行後面的任務(先執行任務2,後執行任務4)。
  • 在併發隊列中添加同步和異步任務
//建立併發隊列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"任務1");
dispatch_async(concurrentQueue, ^{
    NSLog(@"開始任務2");
    sleep(3);
    NSLog(@"任務2--%@",[NSThread currentThread]);
});
NSLog(@"任務3");
dispatch_sync(concurrentQueue, ^{
    NSLog(@"開始任務4");
    sleep(3);
    NSLog(@"任務4--%@",[NSThread currentThread]);
});
NSLog(@"任務5");
複製代碼

執行結果以下:

  1. 異步任務不會阻塞當前線程,因此任務1和任務3先執行,任務2後執行
  2. 併發隊列中多個任務能夠同時執行,所以任務2和任務4併發執行
  3. 異步任務會開闢新的線程,同步任務會在當前線程執行。所以任務2在子線程中執行,任務4在主線程中執行。
  4. 異步任務阻塞當前線程,所以任務4執行完成以後纔會執行任務5

任務和隊列組合執行效果

隊列存在兩種:串行隊列併發隊列,加上系統提供的主隊列總共三種隊列(此處因爲主隊列中添加的任務都會在主線程中執行,所以將主隊列單獨做爲一種特殊的隊列)。

任務又分爲兩種:同步任務異步任務,所以隊列加任務共有6種組合,所產生的效果及對好比下:

串行隊列(手動建立) 主隊列 併發隊列
同步任務(sync) ●不會開闢新線程
●串行執行任務
產生死鎖 ●不會開闢新線程
●串行執行任務
異步任務(async) ●開闢新線程
●串行執行任務
●不會開闢新線程
●串行執行任務
●開闢新線程
●併發執行任務
  • 只有異步任務纔會開啓新的線程
  • 只有異步任務添加到併發隊列中,纔會併發執行任務

還要注意一點:當使用sync向主隊列中添加同步任務時,會產生死鎖。此處暫時不考慮任務嵌套。

死鎖的產生

  • 第一種:在主隊列中添加同步任務會產生死鎖
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"任務1");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"任務2");
    });
    NSLog(@"任務3");
}
複製代碼

執行圖以下

首先,在執行viewDidLoad方法時,實際上是將viewDidLoad添加到主隊列中,由於viewDidLoad如今是在隊首,因此先執行viewDidLoad方法。

viewDidLoad中有3個任務,都是在主線程中執行,當執行完任務1後,經過dispatch_sync方法又向主隊列中添加了任務2(實際上是整個block,這裏暫且稱爲任務2),可是因爲同步任務的特性是必現執行完且返回才能執行後面的任務,所以必需要執行完任務2才能執行後面的任務3

此時在主隊列中存在兩個任務,viewDidLoad任務2任務2想要執行,就必須等待viewDidLoad執行完,而viewDidLoad想要執行完,必需要執行完任務2以及任務3,可是任務3想要執行,就必須執行完任務2,所以任務2在等待viewDidLoad執行完,viewDidLoad又在等待任務2執行完,從而形成死鎖。

  • 第二種:在異步任務中嵌套同步任務,而且是添加到串行隊列中就會產生死鎖
//建立串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{  //此處稱爲block1
    NSLog(@"任務1");
    dispatch_sync(serialQueue, ^{ //此處稱爲block2
        NSLog(@"任務2");
    });
    NSLog(@"任務3");
});
複製代碼

執行圖以下:

首先,經過dispatch_async添加異步任務時會開啓新的線程,因此此時block1中的任務是在子線程中執行,同時由於是在串行隊列中增長的異步任務,因此block1會被添加到串行隊列中去,而且在隊首。

在子線程中執行block1中的方法,先執行任務1,而後執行dispatch_sync方法,此時會向串行隊列中增長同步任務block2,而且須要等到block2執行完成以後纔會執行任務3

此時在串行隊列中存在兩個任務,block1block2block2想要執行,就必須等待block1執行完,而block1想要執行完,必需要執行完block2以及任務3,可是任務3想要執行,又必須執行完block2,所以block1在等待block2執行完,block2又在等待block1執行完,從而形成死鎖。

  • 第三種:同步任務中嵌套同步任務,而且添加到串行隊列中
//建立串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
    NSLog(@"任務1");
    dispatch_sync(queue, ^{
            NSLog(@"任務2");
    });
    NSLog(@"任務3");
});
複製代碼

其實這種死鎖的方式和第一種相似,同步任務仍是在主線程執行,只不過被添加到了自定義的串行隊列中,所以形成死鎖的緣由和第一種基本相同,這裏不作介紹。

GCD的其它用法

柵欄方法:dispatch_barrier_async

柵欄方法主要是在多組操做之間增長柵欄,從而分割多組操做,使得各組操做之間順序執行。例如:有兩組操做,須要執行完第一組操做以後再執行第二組操做,此時就須要用到dispatch_barrier_async,代碼以下:

//建立併發隊列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
    //任務組一
    for (int i = 0; i < 5; i++) {
        dispatch_async(concurrentQueue, ^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"執行組一任務%d",i);
        });
    }
    //柵欄方法
    dispatch_barrier_sync(concurrentQueue, ^{
        NSLog(@"柵欄方法");
    });
    
    //任務組二
    for (int i = 0; i < 5; i++) {
        dispatch_async(concurrentQueue, ^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"執行組二任務%d",i);
        });
    }
複製代碼

前提是全部的任務都須要添加到同一個隊列中

執行結果以下:

能夠看出任務組一中的5個任務併發執行,執行完成以後會先執行柵欄函數,最後纔會執行任務組二中的全部操做,具體以下圖:

還有一點須要注意的是,這個函數傳入的併發隊列必須是經過dispatch_queue_create手動建立的,若是傳入的是一個串行或者一個全局的併發隊列,那麼這個函數的效果等同於dispatch_async函數

隊列組:dispatch_group

隊列組是一個很是實用的功能,它能夠在一組異步任務都執行完成以後,再執行下一步操做。例如:有多個接口,須要等到全部的接口返回結果以後再到主線程更新UI。

隊列組有三種使用方法:

  • 第一種:dispatch_group_async配合dispatch_group_notify
- (void)testGroup1{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"網絡任務1:%@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"網絡任務2:%@", [NSThread currentThread]);
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"主線程更新UI:%@", [NSThread currentThread]);
    });
}
複製代碼
  • 第二種:dispatch_group_enterdispatch_group_leavedispatch_group_notify搭配使用
- (void)testGroup2{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"網絡任務1:%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"網絡任務2:%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"主線程更新UI:%@", [NSThread currentThread]);
    });
}
複製代碼
  • 第三種:dispatch_group_asyncdispatch_group_wait結合使用
- (void)testGroup3{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"網絡任務1:%@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"網絡任務2:%@", [NSThread currentThread]);
    });
    //等待上面的任務所有完成後,會往下繼續執行(會阻塞當前線程)
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"主線程更新UI:%@", [NSThread currentThread]);
    });
}
複製代碼

以上三種方式的執行結果相同,以下:

信號量:dispatch_semaphore

信號量就是一種用來控制訪問資源的數量的標識,當咱們設置了一個信號量,在線程訪問以前加上信號量的處理,就能夠告知系統按照咱們設定的信號量數量來執行多個線程。信號量實際上是用計數來實現的,若是信號量計數小於0,則會一直等待,阻塞線程。若是信號量計數爲0或者大於0,則不等待且計數-1。

GCD提供了三個方法來幫助咱們使用信號量

函數 做用
dispatch_semaphore_create 建立信號量,初始值能夠爲0
dispatch_semaphore_signal 發送信號,信號量計數+1
dispatch_semaphore_wait 若是信號量>0,則使信號量-1,執行後續操做
若是信號量<=0,則會阻塞當前線程,直到信號量>0

示例代碼以下:

- (void)testSemaphore{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    //信號量初始爲0
    dispatch_semaphore_t seq = dispatch_semaphore_create(0);

    NSLog(@"任務1");
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"任務2");
        //信號量+1
        dispatch_semaphore_signal(seq);
    });
    //此時信號量小於0,因此一直等待,當信號量>=0時執行後續代碼
    dispatch_semaphore_wait(seq, DISPATCH_TIME_FOREVER);
    
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"任務3");
        //信號量+1
        dispatch_semaphore_signal(seq);
    });
    //信號量-1
    dispatch_semaphore_wait(seq, DISPATCH_TIME_FOREVER);
    NSLog(@"任務4");
}
複製代碼

執行結果以下:

首先會執行任務1,而後往併發隊列中添加異步任務,以後執行dispatch_semaphore_wait時,信號量-1,此時信號量小於0(初始爲0),所以線程被阻塞,一直在此處等待。當任務2執行完成後,會調用dispatch_semaphore_signal,此時信號量+1,程序繼續往下執行。

所以,信號量也能夠用來實現多個異步任務順序執行,以及多個異步任務所有執行結束以後統一執行某些操做的需求。

NSOperation

NSOperation實際上是對GCD更高一層的封裝,徹底面向對象,使用起來比GCD更加簡單易用,代碼的可讀性也更高。而且NSOperation也提供了一些GCD沒有提供的更加實用的功能。好比:

  • 能夠設置任務(operation)之間的依賴,用來控制多個異步任務順序執行
  • 能夠設置任務(operation)的優先級
  • 能夠取消任務(operation)
  • 能夠設置線程的最大併發數

NSOperation的子類

NSOperation是一個抽象類,不能直接使用。想要使用他的功能,就要使用它的子類NSInvocationOperationNSBlockOperation。也能夠自定義NSOperation的子類。

NSBlockOperation

NSBlockOperation是將任務存放到block中,在合適的時機進行調用。

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"任務1:%@", [NSThread currentThread]);
}];
[operation1 start];
複製代碼

而且NSBlockOperation還能夠經過addExecutionBlock:方法添加額外操做,而且經過addExecutionBlock:添加的任務和經過blockOperationWithBlock:添加的任務能夠在不一樣的線程中併發執行。

- (void)testBlock{
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"主任務:%@",[NSThread currentThread]);
    }];
    [op addExecutionBlock:^{
        NSLog(@"附加任務1:%@",[NSThread currentThread]);
    }];
    [op addExecutionBlock:^{
        NSLog(@"附加任務2:%@",[NSThread currentThread]);
    }];
    [op start];
}
複製代碼

執行結果以下:

經過blockOperationWithBlock:建立的任務默認會在當前線程中同步執行,可是當blockOperationWithBlock:addExecutionBlock:同時使用,而且addExecutionBlock:添加的任務足夠多時,blockOperationWithBlock:建立的任務也會在子線程中執行。

經過addExecutionBlock:添加任務必定會開闢新的線程,在新線程中執行附加任務。

NSInvocationOperation

NSInvocationOperation能夠指定target和selector

- (void)testOp{
    NSInvocationOperation *invocationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(opeartion) object:nil];
    [invocationOp start];
}

- (void)opeartion{
    NSLog(@"任務%@", [NSThread currentThread]);
}
複製代碼

默認狀況下,NSInvocationOperation在調用start方法的時候不會開啓線程,會在當前線程同步執行,只有當operation被添加到NSOperationQueue中才會開啓新線程異步執行操做。

NSOperation依賴設置

NSOperation能夠設置任務之間的依賴,使任務按照預約的依賴順序執行

- (void)testOp{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任務一:%@",[NSThread currentThread]);
    }];
    
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任務二:%@",[NSThread currentThread]);
    }];
    
    NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(testOp) object:nil];
    //任務二依賴任務一
    [op2 addDependency:op1];
    //任務三依賴任務二
    [op3 addDependency:op2];
    [queue addOperations:@[op1, op2, op3] waitUntilFinished:NO];
}

- (void)methond3{
    NSLog(@"任務三:%@",[NSThread currentThread]);
}
複製代碼

本來三個任務是併發執行,可是添加完依賴以後就變成了順序執行,以下:

此時由於3個任務順序執行,因此只需開闢一條線程便可。

NSOperationQueue

NSOperation中也有隊列的概念,就是NSOperationQueue,一般NSOperationQueueNSOperation會結合使用,一旦NSOperation被添加到NSOperationQueue時,會自動開闢新的線程異步執行

- (void)testOperation{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任務1:%@", [NSThread currentThread]);
    }];
    [operation1 start];
    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任務2, %@", [NSThread currentThread]);
    }];
    [queue addOperation:operation2];
}
複製代碼

執行結果以下:

能夠看到,任務1沒有添加到NSOperationQueue中,在主線程中執行,任務2添加到NSOperationQueue中,在子線程中執行。

注意:NSOperation添加到NSOperationQueue後會自動執行start方法,無需手動調用。

  • NSOperationQueue設置任務最大併發數
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//設置最大併發數
queue.maxConcurrentOperationCount = 1;
for (int i = 0; i < 5; i++) {
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任務%d,%@",i, [NSThread currentThread]);
    }];
    [queue addOperation:op];
}
複製代碼

代碼中將最大併發數設置爲1,任務就會順序執行,結果以下:

  • NSOperationQueue能夠取消/掛起/恢復隊列操做
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//掛起任務
queue.suspended = YES;
//恢復任務
queue.suspended = NO;
//取消隊列中全部任務(已經開始的沒法取消)
[queue cancelAllOperations];
複製代碼
  • NSOperationQueue能夠經過如下方式獲取主隊列和當前隊列
//獲取當前隊列
[NSOperationQueue currentQueue];
//獲取主隊列
[NSOperationQueue mainQueue];
複製代碼

NSOperation總結

NSOperation屬性和方法總結

  • 操做優先級
//設置操做優先級
@property NSOperationQueuePriority queuePriority;
複製代碼
  • 操做狀態判斷
//操做是否正在執行
@property (readonly, getter=isExecuting) BOOL executing;
//操做是否完成
@property (readonly, getter=isFinished) BOOL finished;
//操做是不是併發執行
@property (readonly, getter=isConcurrent) BOOL concurrent; 
//操做是不是異步執行
@property (readonly, getter=isAsynchronous) BOOL asynchronous;
//操做是否準備就緒
@property (readonly, getter=isReady) BOOL ready;
複製代碼
  • 取消操做
//操做是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;
//取消操做
- (void)cancel;
複製代碼
  • 操做同步
//添加任務依賴
- (void)addDependency:(NSOperation *)op;
//移除任務依賴
- (void)removeDependency:(NSOperation *)op;
//獲取當前任務的全部依賴
@property (readonly, copy) NSArray<NSOperation *> *dependencies;

//阻塞任務執行線程,直到該任務執行完成
- (void)waitUntilFinished;

//在當前任務執行完成以後調用completionBlock
@property (nullable, copy) void (^completionBlock)(void);
複製代碼

NSOperationQueue屬性和方法總結

  • 添加任務
//添加單挑任務
- (void)addOperation:(NSOperation *)op;
//添加多個任務
- (void)addOperations:(NSArray<NSOperation *> *)ops;
//直接向隊列中添加一個NSBlockOperation類型的操做
- (void)addOperationWithBlock:(void (^)(void))block;
//在隊列中的全部任務都執行完成以後會執行barrier block,相似柵欄
- (void)addBarrierBlock:(void (^)(void))barrier;
複製代碼
  • 最大併發數
//設置最大併發數
@property NSInteger maxConcurrentOperationCount;
複製代碼
  • 隊列狀態
//掛起\恢復隊列操做  YES:掛起  NO:恢復
@property (getter=isSuspended) BOOL suspended;
//取消隊列中全部操做
- (void)cancelAllOperations;
//阻塞當前線程,直到隊列中的操做所有執行完
- (void)waitUntilAllOperationsAreFinished;
複製代碼
  • 獲取隊列
//獲取當前隊列
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue;
//獲取主隊列
@property (class, readonly, strong) NSOperationQueue *mainQueue;
複製代碼

多線程存在的安全隱患

在單線程條件下,任務都是串行執行,因此不存在安全問題,多線程可以極大的提升程序運行效率,可是多線程也存在隱患。當多個線程訪問同一塊資源時,很是容易引起數據錯亂和數據安全問題。例如:如今有兩條線程同時訪問和修改同一個變量,以下:

線程A和線程B同時讀取Integer的值,都爲17,而後又同時對Integer的值+1,以後在修改Integer的值時因爲線程A和線程B併發執行,所以兩個線程會同時將Integer的值改成18,從而致使數據錯亂。解決辦法就是使用線程同步技術,就是讓線程按預約的前後順序依次執行。常見的線程同步技術是:加鎖。以修改Integer的值爲例,使用線程同步技術後結果以下:

線程A在訪問Integer前先進行加鎖操做,此時線程B沒法訪問Integer,而後線程A讀取Integer的值,改成18,而後進行解鎖,此時線程B就可以訪問Integer,先進行加鎖,讀取Integer值爲18,而後修改成19,最後再解鎖。所以,使用加鎖技術,就可以解決多線程的安全問題。

iOS線程同步方案

iOS中常見的線程同步技術有如下幾種,咱們以一個簡單的Demo來對比一下這幾種線程同步技術。

示例:假設如今銀行帳戶上有5000元,使用多線程,分屢次在銀行帳戶上存錢取錢,保證最後銀行存款正確。

若是咱們使用多線程可是不使用線程同步技術的話,代碼以下:

- (void)moneyTest{
    __block int totalMoney = 5000;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [NSThread sleepForTimeInterval:2];
            totalMoney+=100;
            NSLog(@"存100,帳戶餘額:%d",totalMoney);
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [NSThread sleepForTimeInterval:2];
            totalMoney-=200;
            NSLog(@"取200,帳戶餘額:%d",totalMoney);
       
        }
    });
}
複製代碼

若是按正常的流程,通過5次存錢和5次取錢,帳戶餘額應該最終變爲4500元,可是最終執行的結果缺大不相同,以下:

整個過程當中帳戶餘額的計算都有問題,同時,通過10次存取以後,帳戶餘額還剩4700元,這就是多線程使用帶來的弊端。

如今,咱們就用如下技術來解決存錢取錢的問題

如下各類鎖的實現能夠在GNUstep中找到相應實現,雖然GNUstep不是官方源碼,可是也具備必定的參考價值。

OSSpinLock

OSSpinLock叫作「自旋鎖」,顧名思義,線程在等待解鎖的過程當中會處於忙等狀態,而且一直會佔用CPU資源。

OSSpinLock如今已經再也不安全,由於它會出現優先級反轉的問題,即優先級低的線程首先得到鎖,進行加鎖操做,CPU會給它分配資源來執行後續任務,若是此時有高優先級的線程進入,那麼CPU會優先給高優先級的線程分配資源,此時低優先級線程得不到資源沒法釋放鎖,而高優先級線程因爲在等待低優先級線程解鎖,並且是處於忙等狀態,一直佔用着CPU資源。所以就致使優先級反轉的問題。

OSSpinLock具體Api以下:

#import <libkern/OSAtomic.h>

//初始化鎖
OSSpinLock lock = OS_SPINLOCK_INIT;
//嘗試加鎖(若是須要等待,就不加鎖,直接返回false,若是不須要等待就加鎖,而且返回true)
bool result = OSSpinLockTry(&lock);
//加鎖
OSSpinLockLock(&lock);
//解鎖
OSSpinLockUnlock(&lock)
複製代碼

回到上述Demo,對存錢取錢操做進行加鎖,以下:

- (void)moneyTest{
    //初始化鎖
    __block OSSpinLock lock = OS_SPINLOCK_INIT;
    __block int totalMoney = 5000;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            OSSpinLockLock(&lock);  //加鎖
            [NSThread sleepForTimeInterval:.1];
            totalMoney+=100;
            OSSpinLockUnlock(&lock);//解鎖
            NSLog(@"存100,帳戶餘額:%d",totalMoney);
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            OSSpinLockLock(&lock);  //加鎖
            [NSThread sleepForTimeInterval:.1];
            totalMoney-=200;
            OSSpinLockUnlock(&lock);//解鎖
            NSLog(@"取200,帳戶餘額:%d",totalMoney);
       
        }
    });
}
複製代碼

運行結果以下:

能夠看到,整個過程按照順序依次執行,先進行存錢,後進行取錢,最終帳戶餘額爲4500元,解決了數據錯亂的問題。

os_unfair_lock

os_unfair_lock被用來取代OSSpinLock,而且從iOS 10開始支持os_unfair_lock。等待鎖的線程會處於休眠狀態(不一樣於OSSpinLock的忙等狀態),不會佔用CPU資源。所以,使用os_unfair_lock不會致使優先級反轉的問題。

os_unfair_lockApi以下:

#import <os/lock.h>

//初始化鎖
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//嘗試加鎖
bool result = os_unfair_lock_trylock(&lock);
//加鎖
os_unfair_lock_lock(&lock);
//解鎖
os_unfair_lock_unlock(&lock);

複製代碼

使用方式同OSSpinLock

pthread_mutex

pthread_mutex稱爲互斥鎖,即當一個線程得到某一共享資源的使用權以後,會將該資源進行加鎖,若是此時有其它線程想要獲取該資源的鎖,那麼它將會被阻塞進入睡眠狀態,直到該資源被解鎖後纔會喚醒。若是有多個線程嘗試獲取該資源的鎖,那麼它們都會進入睡眠狀態,一旦該資源被解鎖,這些線程就都會被喚醒,可是真正能得到資源使用權的是第一個被喚醒的線程。

使用互斥鎖的線程在等待鎖的過程當中會處於休眠狀態,不會佔用CPU資源

pthread_mutex的Api以下:

#import <pthread.h>

/*
 * 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
    
    
//初始化鎖屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    
//初始化鎖,第二個參數能夠傳NULL,就是使用默認的屬性
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
    
//嘗試加鎖
pthread_mutex_trylock(&mutex);
//加鎖
pthread_mutex_lock(&mutex);
//解鎖
pthread_mutex_unlock(&mutex);
    
//銷燬
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);
複製代碼

普通鎖

使用pthread_mutex對存錢取錢進行加鎖,以下:

- (void)moneyTest{
    //初始化鎖
    __block pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);
    __block int totalMoney = 5000;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            pthread_mutex_lock(&mutex);  //加鎖
            [NSThread sleepForTimeInterval:.1];
            totalMoney+=100;
            pthread_mutex_unlock(&mutex);//解鎖
            NSLog(@"存100,帳戶餘額:%d",totalMoney);
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            pthread_mutex_lock(&mutex);  //加鎖
            [NSThread sleepForTimeInterval:.1];
            totalMoney-=200;
            pthread_mutex_unlock(&mutex);//解鎖
            NSLog(@"取200,帳戶餘額:%d",totalMoney);
       
        }
    });
    //在不使用鎖時須要調用此方法對鎖進行銷燬
    //pthread_mutexattr_destroy(&mutex);
}

複製代碼

遞歸鎖

在初始化鎖時,咱們能夠指定鎖的類型爲PTHREAD_MUTEX_RECURSIVE,此時咱們就建立了一個遞歸鎖遞歸鎖是指同一個線程能夠屢次得到某一個共享資源的鎖(屢次進行加鎖操做),別的線程想要獲取該資源鎖,就必須等待該線程釋放全部次數的鎖。下面咱們就建立一個遞歸函數的Demo來了解一下遞歸鎖的使用:

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

@interface XLMutexRecursiveTest ()

@property(nonatomic, assign)pthread_mutex_t mutex;

@end

@implementation XLMutexRecursiveTest

- (instancetype)init
{
    self = [super init];
    if (self) {
        //遞歸鎖:容許同一個線程對同一把鎖進行重複加鎖
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        //pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        //初始化鎖
        pthread_mutex_init(&_mutex, &attr);
        //銷燬屬性
        pthread_mutexattr_destroy(&attr);
    }
    return self;
}

- (void)recursiveTask{
    
    pthread_mutex_lock(&_mutex);
    
    NSLog(@"recursiveTask");
    static int count = 0;
    if (count < 5) {
        count++;
        [self recursiveTask];
    }
    
    pthread_mutex_unlock(&_mutex);
}

- (void)dealloc{
    pthread_mutex_destroy(&_mutex);
}

@end
複製代碼

首先建立普通鎖PTHREAD_MUTEX_NORMAL,而後實例化XLMutexRecursiveTest實例進行調用

XLMutexRecursiveTest *recursiveTest = [[XLMutexRecursiveTest alloc] init];
[recursiveTest recursiveTask];
複製代碼

執行以後發現程序會一直卡死在第一次打印NSLog的地方。這是由於當首次執行recursiveTask方法時會對_mutex進行加鎖,而後執行NSLog,當count < 5時,會再次執行recursiveTask方法,此時會發現_mutex已經被加鎖了,所以第二次執行的recursiveTask方法會一直在等待解鎖,而第一次執行的recursiveTask方法想要解鎖,就必需要等第二次的任務執行完成,所以就形成了死鎖

下面咱們將鎖改爲遞歸鎖,從新執行,會發現全部的任務都正常打印了,以下

注意:在不使用pthread_mutex時要調用pthread_mutexattr_destroypthread_mutex_destroy對鎖及其屬性進行銷燬。

條件變量

條件變量是在多線程中用來實現「等待->喚醒」邏輯的經常使用方式,相似於GCD中的信號量。條件變量是利用一個全局共享變量來進行線程同步。它主要分爲三步:

  • 線程一等待條件變量的條件成立而被掛起
  • 線程二使條件變量成立
  • 喚醒線程一

而且爲了防止資源競爭,一般將條件變量和互斥鎖結合使用。由於條件變量一般是多個線程或者進程的共享變量,因此就極有可能產生資源競爭,因此在使用條件變量以前須要對其加上互斥鎖。pthread_mutex關於條件變量使用的Api以下:

//初始化鎖
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
//初始化條件
pthread_cond_t condt;
pthread_cond_init(&condt, NULL);
//等待條件(此時會進入休眠狀態,同時對mutex進行解鎖,被再次喚醒後,會對mutex再次加鎖)
pthread_cond_wait(&condt, &mutex);
//激活一個等待該條件的線程
pthread_cond_signal(&condt);
//激活全部等待該條件的線程
pthread_cond_broadcast(&condt);
        
//銷燬
pthread_cond_destroy(&condt);
pthread_mutex_destroy(&mutex);
複製代碼

條件變量比較典型的應用即是生產者-消費者模式,下面就模擬生產者-消費者來建立一個簡單的Demo瞭解一下條件變量互斥鎖的使用,代碼以下:

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

@interface XLMutexConditionLockTest ()
//杯子餘量
@property(nonatomic, strong)NSMutableArray *cupsRemain;
@property(nonatomic, assign)pthread_mutex_t mutex;
@property(nonatomic, assign)pthread_cond_t condt;

@end

@implementation XLMutexConditionLockTest

- (instancetype)init
{
    self = [super init];
    if (self) {
        //初始化鎖
        pthread_mutex_init(&_mutex, NULL);
        //初始化條件
        pthread_cond_init(&_condt, NULL);
    }
    return self;
}

- (void)testSaleAndProduce{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self _saleCup];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self _produceCup];
    });
}

//出首杯子
- (void)_saleCup{
    pthread_mutex_lock(&_mutex);
    if (self.cupsRemain.count == 0) {
        //若是杯子餘量爲0,則等待生產杯子
        NSLog(@"當前無可用杯子庫存");
        pthread_cond_wait(&_condt, &_mutex);
    }
    //此時有可出售的杯子
    [self.cupsRemain removeLastObject];
    NSLog(@"售出一個杯子");
    pthread_mutex_unlock(&_mutex);
}

//生產杯子
- (void)_produceCup{
    pthread_mutex_lock(&_mutex);
    
    //睡眠兩秒,模擬生產過程
    sleep(2);
    [self.cupsRemain addObject:@"yellow cup"];
    NSLog(@"生產了一個黃色杯子");
    //通知條件變量成立
    pthread_cond_signal(&_condt);
    
    pthread_mutex_unlock(&_mutex);
}

- (void)dealloc{
    //銷燬
    pthread_cond_destroy(&_condt);
    pthread_mutex_destroy(&_mutex);
}

@end
複製代碼

執行結果以下:

能夠發現,雖然_produceCup方法睡眠2s執行,可是_saleCup方法仍是等待_produceCup執行完成以後再執行。由此能夠總結出整個條件變量的流程以下:

  • 首先,前後在不一樣線程調用saleCup和produceCup的方法。
  • saleCup所在線程先獲取mutex,對其進行加鎖,而後判斷是否有庫存,此時庫存爲0,因此調用pthread_cond_wait方法,pthread_cond_wait方法主要分爲三步:
    • 將當前線程放入等待條件知足的線程隊列中。
    • mutex進行解鎖
    • 掛起(阻塞)當前線程,等待被喚醒。(此時pthread_cond_wait函數並未返回)
  • 調用produceCup方法,因爲mutex此時已經被解鎖,因此produceCup所在線程能夠對其進行加鎖,而後向數組中增長一個元素,以後調用pthread_cond_signal方法激活saleCup所在線程,最後調用pthread_mutex_unlock方法對mutex解鎖。
  • 接收到pthread_cond_signal信號後,saleCup所在線程被激活,同時pthread_cond_wait函數返回,在pthread_cond_wait函數返回時會自動對mutex進行再次加鎖。
  • 移除數組中的最後一個元素,最後對mutex進行解鎖。

dispatch_semaphore

dispatch_semaphore叫作信號量,前面講GCD的時候也介紹過。dispatch_semaphore是經過設置一個全局的信號量,來控制線程併發訪問的最大數量。假設信號量初始值爲1,那麼表明同時只容許1條線程訪問資源,以此來保證線程同步。使用方式以下:

- (void)testDispatch{
    //設置信號初始值
    int semaphoreValue = 1;
    //初始化信號量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(semaphoreValue);
    __block int totalMoney = 5000;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            //若是此時信號量<=0,那麼dispatch_semaphore_wait會讓線程處於休眠等待狀態,直到信號量>0
            //若是信號量>0,則執行dispatch_semaphore_wait會使信號量-1
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            [NSThread sleepForTimeInterval:.1];
            totalMoney+=100;
            //會對信號量進行+1操做
            dispatch_semaphore_signal(semaphore);
            NSLog(@"存100,帳戶餘額:%d",totalMoney);
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//信號量-1
            [NSThread sleepForTimeInterval:.1];
            totalMoney-=200;
            dispatch_semaphore_signal(semaphore);
            NSLog(@"取200,帳戶餘額:%d",totalMoney);//信號量+1
       
        }
    });
    
}
複製代碼

初始信號量的值爲1,此時,調用dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)方法,會判斷信號量是否>0,若是信號量>0則會執行後續的操做,而且將信號量的值-1。若是信號量<=0,那麼此方法會使當前線程處於休眠等待狀態,直到信號量的值>0。

調用dispatch_semaphore_signal(semaphore)會使信號量+1,兩種方法搭配使用就能實現線程同步的效果。

dispatch_queue(DISPATCH_QUEUE_SERIAL)

dispatch_queue(DISPATCH_QUEUE_SERIAL)其實就是一個串行隊列,上文也說過,無論往串行隊列中添加同步任務仍是異步任務,在執行時都是串行執行任務。使用方式以下

- (void)testDispatchQueue{
    //建立串行隊列
    dispatch_queue_t queue = dispatch_queue_create("lock_queue", DISPATCH_QUEUE_SERIAL);
    __block int totalMoney = 5000;
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [NSThread sleepForTimeInterval:.2];
            totalMoney+=100;
            NSLog(@"存100,帳戶餘額:%d",totalMoney);
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [NSThread sleepForTimeInterval:.2];
            totalMoney-=200;
            NSLog(@"取200,帳戶餘額:%d",totalMoney);//信號量+1
       
        }
    });
}
複製代碼

NSLock && NSRecursiveLock && NSCondition && NSConditionLock

NSLockNSRecursiveLockNSConditionNSConditionLock實際上是對pthread_mutex中普通鎖、遞歸鎖和條件變量的封裝,使其面向對象,使用起來更加簡單。使用方式其實和pthread_mutex差很少,這裏不作單獨介紹了,只列出經常使用Api

NSLock

@protocol NSLocking

//加鎖
- (void)lock;
//解鎖
- (void)unlock;

@end

@interface NSLock : NSObject <NSLocking>
//嘗試加鎖
- (BOOL)tryLock;
//給鎖設置到期時間
- (BOOL)lockBeforeDate:(NSDate *)limit;
@end

複製代碼

NSRecursiveLock

@interface NSRecursiveLock : NSObject <NSLocking> 
//嘗試加鎖
- (BOOL)tryLock;
//給鎖設置到期時間
- (BOOL)lockBeforeDate:(NSDate *)limit;
@end
複製代碼

NSCondition

NSCondition 實際上是封裝了一個互斥鎖和條件變量, 它把前者的 lock 方法和後者的 wait/signal 統一在 NSCondition 對象中,暴露給使用者。它的加鎖和解鎖過程同NSLock一致

@interface NSCondition : NSObject <NSLocking>

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@end
複製代碼

NSConditionLock

NSConditionLock是對NSCondition的再一次封裝,與NSCondition不一樣的是NSConditionLock能夠設置具體的條件值

@interface NSConditionLock : NSObject <NSLocking> 
//帶條件加鎖
- (void)lockWhenCondition:(NSInteger)condition;
//嘗試加鎖
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
//帶條件解鎖
- (void)unlockWithCondition:(NSInteger)condition;
//設置鎖到期時間
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@end
複製代碼

@synchronized

@synchronized內部其實封裝了一個mutex遞歸鎖。傳入一個obj參數,內部會自動生成obj對應的遞歸鎖,而且存放在哈希表中。經過obj的內存地址到哈希表中能拿到obj對應的遞歸鎖。想要了解@synchronized內部實現,能夠下載objc源碼,查看objc_sync.mm文件中的objc_sync_enterobjc_sync_exit函數。

@synchronized的使用很簡單,以下:

@synchronized (obj) {
    //須要加鎖的代碼
}
複製代碼

@synchronized應用到存錢取錢的案例中,以下:

- (void)testSynchronized{
    __block int totalMoney = 5000;
    NSObject *obj = [NSObject new];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (obj) {
            for (int i = 0; i < 5; i++) {
                [NSThread sleepForTimeInterval:.2];
                totalMoney+=100;
                NSLog(@"存100,帳戶餘額:%d",totalMoney);
            }
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (obj) {
            for (int i = 0; i < 5; i++) {
                [NSThread sleepForTimeInterval:.2];
                totalMoney-=200;
                NSLog(@"取200,帳戶餘額:%d",totalMoney);
            }
        }
    });
}
複製代碼

傳入的obj必須有值,若是obj傳nil,則@synchronized(nil)不起任何做用。同時要實現多線程同步的話,就必須傳入相同的obj

各類鎖性能對比

借用大神ibireme的再也不安全的 OSSpinLock一文中關於各類鎖的性能對比圖,以下:

鎖相關知識補充

經過彙編代碼辨別是自旋鎖仍是互斥鎖

分辨自旋鎖和互斥鎖的方式,能夠根據等待鎖的過程當中,線程是休眠仍是忙等狀態來區分。若是線程是休眠狀態。就是互斥鎖,若是是忙等狀態,就是自旋鎖。在OC中能夠跟蹤彙編代碼來判斷一個鎖是自旋鎖仍是互斥鎖。以OSSpinLockos_unfair_lock爲例來進行彙編代碼跟蹤:

#import "XLLockTest.h"
#import <libkern/OSAtomic.h>
#import <os/lock.h>

@interface XLLockTest ()
@property(nonatomic, assign)OSSpinLock lock;
@end

@implementation XLLockTest

- (instancetype)init{
    self = [super init];
    if (self) {
        _lock = OS_SPINLOCK_INIT;
    }
    return self;
}

- (void)test{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self thread2];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self thread1];
    });
}

- (void)thread1{
    OSSpinLockLock(&_lock);
    NSLog(@"thread1");
    OSSpinLockUnlock(&_lock);
}

- (void)thread2{
    OSSpinLockLock(&_lock);
    sleep(60);
    NSLog(@"thread2");
    OSSpinLockUnlock(&_lock);
}
複製代碼

OSSpinLock

斷點在thread1方法,調用test方法,使用LLDB指令si一步一步執行彙編代碼。首先進入OSSpinLockLock方法

在OSSpinLockLock方法內部調用了_OSSpinLockLockSlow函數

進入_OSSpinLockLockSlow函數,執行si指令,會發現,程序一直在循環執行一段彙編指令,以下:

熟悉彙編的同窗能夠看出其實這一段彙編代碼就是一個while循環,由此就能夠看出OSSpinLock屬於自旋鎖。

os_unfair_lock

將Demo中的鎖換成os_unfair_lock,而後用相同的方式跟蹤彙編代碼。首先是進入os_unfair_lock_lock方法,方法內部會調用_os_unfair_lock_lock_slow函數

_os_unfair_lock_lock_slow函數內部會調用__ulock_wait函數

在__ulock_wait函數內部會調用syscall,syscall其實就是系統級別的函數,執行完syscall函數以後,當前線程就會進入休眠狀態。

由此就能夠看出os_unfair_lock屬於互斥鎖。

自旋鎖和互斥鎖對比

自旋鎖

自旋鎖其實就是指當一個線程獲取到資源鎖以後,其它線程在獲取資源鎖時,會一直處於忙等狀態(busy-waiting)。處於忙等狀態的線程會一直處於活躍狀態,可是內部並無執行任何有效的任務,只是一直在循環查看資源鎖擁有者是否已經釋放了鎖。

如下狀況下適合使用自旋鎖

  • 線程等待鎖的時間很短
  • 加鎖的代碼(臨界區)常常被調用,可是發生競爭的狀況不多。
  • 在CPU資源充足的狀況下使用自旋鎖效率更高
  • 多核處理器也適合使用自旋鎖

互斥鎖

互斥鎖則是指當一個線程獲取到資源鎖以後,其它線程在獲取資源鎖時會被阻塞,進入睡眠狀態(sleep-waiting)。線程休眠以後不會佔用CPU資源,直到資源鎖被釋放以後纔會喚醒線程。

如下狀況下適合使用互斥鎖

  • 預計線程等待鎖的時間較長
  • 單核處理器適合使用互斥鎖
  • 臨界區有IO操做時使用互斥鎖
  • 臨界區代碼複雜,或者有較大循環量的時候使用互斥鎖
  • 臨界區資源競爭狀況不少,競爭激烈的狀況使用互斥鎖

OC中的atomic屬性

在OC中可使用atomic或者nonatomic來修飾屬性,表明原子性和非原子性。其實通俗一點來講,使用atomic修飾的屬性是線程安全的,而使用nonatomic修飾的屬性不是線程安全的。爲何說atomic修飾的屬性是線程安全的呢?查看objc源碼中的objc-accessors.mm文件能夠看到atomic的底層實現,經過閱讀源碼能夠發現,atomic修飾屬性其實就是給屬性的setter和getter方法內部增長了自旋鎖,源碼以下:

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    ......
    if (!atomic) return *slot;
    //從全局的哈希表中獲取到自旋鎖
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    return objc_autoreleaseReturnValue(value);
}

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) {
    ......
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        //從全局的哈希表中獲取到自旋鎖
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    ......
}
複製代碼

可是atomic只是保證了getter和setter存取方法的線程安全,並不能保證整個對象是線程安全的。假設咱們使用atomic修飾NSArray類型的屬性

@property(atomic, strong)NSArray *sourceArray;
複製代碼

若是多個線程對sourceArray進行添加數據操做,確定會產生內存問題,由於atomic只是針對sourceArray自己的getter和setter方法,若是使用[_sourceArray objectAtIndex:index]時,就不是線程安全的,由於它和sourceArray的setter和getter方法沒有關係。想要保證[_sourceArray objectAtIndex:index]的線程安全,就須要對_sourceArray的使用進行加鎖操做。

iOS中的讀寫安全方案

在開發過程當中個,有一種比較特殊的狀況,就是在臨界區中有I/O操做時,若是咱們使用以上任何一種鎖來對臨界區進行加鎖,那麼在同一時間內只能執行一次讀或者寫的操做。可是多條線程同時執行讀的操做是不會有任何數據問題的,只有在多條線程同時執行讀寫操做時纔會形成數據問題。總結來講,就是要知足如下的幾種場景:

  • 同一時間內,只能有一個線程進行寫的操做。
  • 同一時間內,容許有多個線程進行讀的操做。
  • 同一時間內,不容許同時有讀的操做,又有寫的操做。

以上的場景就是典型的「多讀單寫」的操做,常用在文件等數據的讀寫操做。在iOS中想要實現這種效果,經常使用的方案有如下兩種:

  • pthread_rwlock 讀寫鎖
  • dispatch_barrier_async 異步柵欄調用

關於dispatch_barrier_async的使用,上文GCD的部分有詳細介紹,此處主要介紹pthread_rwlock的使用。

pthread_rwlock主要Api以下:

- (void)testRwLock{
    //初始化鎖
    pthread_rwlock_t rwLock;
    pthread_rwlock_init(&rwLock, NULL);
    //讀操做-加鎖
    pthread_rwlock_rdlock(&rwLock);
    //讀操做-嘗試加鎖
    pthread_rwlock_tryrdlock(&rwLock);
    //寫操做-加鎖
    pthread_rwlock_wrlock(&rwLock);
    //讀操做-嘗試加鎖
    pthread_rwlock_trywrlock(&rwLock);
    
    //解鎖
    pthread_rwlock_unlock(&rwLock);
    //銷燬
    pthread_rwlock_destroy(&rwLock);
}
複製代碼

模擬讀寫操做,代碼以下:

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

@interface XLLockTest ()
@property(nonatomic, assign)pthread_rwlock_t rwlock;
@property (strong, nonatomic) dispatch_queue_t queue;
@end

@implementation XLLockTest

- (instancetype)init{
    self = [super init];
    if (self) {
        pthread_rwlock_init(&_rwlock, NULL);
        self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

- (void)test{
    for (int i = 0; i < 10; i++) {
        dispatch_async(self.queue, ^{
            [self readThread];
        });
        
        dispatch_async(self.queue, ^{
            [self writeThread];
        });
    }
}

- (void)readThread{
    pthread_rwlock_rdlock(&_rwlock);
    sleep(1);
    NSLog(@"讀操做");
    pthread_rwlock_unlock(&_rwlock);
}

- (void)writeThread{
    pthread_rwlock_wrlock(&_rwlock);
    sleep(1);
    NSLog(@"寫操做");
    pthread_rwlock_unlock(&_rwlock);
}

- (void)dealloc{
    pthread_rwlock_destroy(&_rwlock);
}

複製代碼

調用XLLockTest的test方法,打印以下:

能夠看出,在同一時間內,可能會執行兩次讀操做,可是隻會執行一次寫操做。

參考文章

再也不安全的 OSSpinLock

深刻理解iOS開發中的鎖

結束語

以上內容純屬我的理解,若是有什麼不對的地方歡迎留言指正。

一塊兒學習,一塊兒進步~~~

相關文章
相關標籤/搜索