iOS 多線程的一些知識

多線程的基本概念

  • 進程:可以理解成一個運行中的應用程序,是系統(tǒng)進行資源分配和調(diào)用的基本單位,是操作系統(tǒng)結(jié)構(gòu)的基礎(chǔ),主要管理資源。
  • 【線程】:進程的基本執(zhí)行單元,一個進程擁有至少一個線程。
  • 【主線程】:處理UI,所有更新UI的操作都必須在主線程上執(zhí)行。
  • 【多線程】:在同一時刻,一個CPU只能處理1條線程,但CPU可以在多條線程之間快速的切換,只要切換的速度足夠快,就造成了多線程一同執(zhí)行的假象。

線程就像火車的一節(jié)車廂,進程則是火車。車廂(線程)離開火車(進程)是無法跑動的,而火車(進程)至少有一節(jié)車廂(主線程)。多線程可以看作多個車廂,他的出現(xiàn)是為了提高效率,另外我們可以利用多線程,將耗時的操作放到后臺執(zhí)行,以防止耗時操作阻塞主線程,導致UI卡頓等現(xiàn)象。

線程的狀態(tài)與生命周期

下圖是線程狀態(tài)示意圖,從圖中可以看出線程的生命周期:新建->就緒->運行->阻塞->死亡

線程狀態(tài)示意圖

下面分別闡述線程的生命周期中的每一步:

  • 【新建】:實例化線程對象。
  • 【就緒】:向線程對象發(fā)送start消息,線程對象被加入可調(diào)度線程池等待CPU調(diào)度。
  • 【運行】:CPU負責調(diào)度可調(diào)度線程池中線程的執(zhí)行。線程執(zhí)行完成之前,狀態(tài)可能會在就緒和運行之間來回切換。就緒和運行之間的狀態(tài)變化由CPU負責,程序員不能干預。
  • 【阻塞】:當滿足某個預定條件時,可以使用休眠或鎖,阻塞線程執(zhí)行。sleepForTimeInterval(休眠指定時長),sleepUntilDate(休眠到指定日期)@synchronized(self),:(互斥鎖)。
  • 【死亡】:正常死亡,線程執(zhí)行完畢。非正常死亡,當滿足某個條件后,在線程內(nèi)部終止執(zhí)行/在主線程終止線程對象。

補充:

[NSThread exit]:一旦強行終止程序,后續(xù)的所有代碼都不會被執(zhí)行。
[thread cancel]:取消線程,并不會直接取消線程,只是給線程對象添加 isCancellled 標記。

多線程的四種解決方案

多線程的四種解決方案:pthread、NSThread、GCD、NSOperation

四種解決方案

NSThread

線程實例

創(chuàng)建一個線程

系統(tǒng)提供了三種創(chuàng)建線程的方法

- (instancetype)init;
// 通過指定對象和方法選擇器的方式,argument是傳遞的參數(shù)
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
// 注意,這種方式是iOS10之后才增加的,將任務放在了block種執(zhí)行
- (instancetype)initWithBlock:(void (^)(void))block;

可設置的一些信息

@property (nullable, copy) NSString *name;  // 線程名稱
@property NSUInteger stackSize; // 棧的大小,4k的倍數(shù),在線程start之前設置
@property double threadPriority; // 線程的優(yōu)先級,【0-1】,值越大,優(yōu)先級別越高,該屬性不久的將來會廢棄,可使用下面的屬性
@property NSQualityOfService qualityOfService;  // 線程優(yōu)先級,iOS8.0之后新增,有一下幾種

NSQualityOfService優(yōu)先級分類

NSQualityOfServiceUserInteractive // 與用戶交互的任務,這些任務通常跟UI級別的刷新相關(guān),比如動畫,這些任務需要在一瞬間完成
NSQualityOfServiceUserInitiated // 由用戶發(fā)起的并且需要立即得到結(jié)果的任務,比如滑動scroll view時去加載數(shù)據(jù)用于后續(xù)cell的顯示,這些任務通常跟后續(xù)的用戶交互相關(guān),在幾秒或者更短的時間內(nèi)完成
NSQualityOfServiceUtility // 一些可能需要花點時間的任務,這些任務不需要馬上返回結(jié)果,比如下載的任務,這些任務可能花費幾秒或者幾分鐘的時間
NSQualityOfServiceBackground // 這些任務對用戶不可見,比如后臺進行備份的操作,這些任務可能需要較長的時間,幾分鐘甚至幾個小時
NSQualityOfServiceDefault // 優(yōu)先級介于user-initiated 和 utility,當沒有 QoS信息時默認使用,開發(fā)者不應該使用這個值來設置自己的任務

線程管理

- (void)start; // 啟動線程
- (void)cancel; // 取消線程

線程的狀態(tài)

@property (readonly, getter=isExecuting) BOOL executing; // 線程是否正在執(zhí)行
@property (readonly, getter=isFinished) BOOL finished; // 線程是否完成
@property (readonly, getter=isCancelled) BOOL cancelled; // 線程是否已經(jīng)取消

實例

  • 手動創(chuàng)建線程
// 創(chuàng)建線程
NSThread *thread = [[NSThread alloc] initWithBlock:^{
    NSLog(@"執(zhí)行線程任務。。。");
}];
// 設置一些屬性
thread.name = @"線程1";
// 開啟線程
[thread start];
NSLog(@"線程信息:%@",thread);
// 取消線程
[thread cancel];
NSLog(@"線程是否被取消:%i",thread.isCancelled);
  • 傳遞一些參數(shù)

如果需要在線程任務中傳遞一些參數(shù),我們需要使用另外一種創(chuàng)建方式。

// 創(chuàng)建線程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@{@"name":@"LOITA0164",@"age":@"25"}];
// 開啟線程
[thread start];

-(void)run:(id)object{
    NSLog(@"傳遞過來的參數(shù):%@",object);
}

小結(jié)

當我們創(chuàng)建NSThread實例后,我們?yōu)槠湓O置一些屬性,例如線程名稱、線程優(yōu)先級、棧大小等信息,與其相對的,我們需要手動管理線程的開始和取消等操作。

創(chuàng)建線程

NSThread類不僅為我們提供了創(chuàng)建實例的方法,另外為我們提供了快捷使用線程的類方法,當你使用這類方法時,你的關(guān)注點不再是線程的創(chuàng)建和開啟,而僅僅是想讓線程去執(zhí)行某些任務,另外,類的方法會自動開始執(zhí)行線程任務。

注:使用類方法時,針對的都是當前的線程

同樣的,有兩種快捷使用方式,一種是通過block方式執(zhí)行線程任務,另一種是通過對象和方法選擇器實現(xiàn)線程任務。

// block方式執(zhí)行任務
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
// 方法選擇器執(zhí)行任務
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

示例

[NSThread detachNewThreadWithBlock:^{
    NSLog(@"執(zhí)行新線程任務");
}];

[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@[@"apple",@"pear"]];

-(void)run:(id)object{
    NSLog(@"傳遞過來的參數(shù):%@",object);
}

一些類方法

NSThread提供了一些類方法,管理線程的調(diào)度,獲取當前的線程,線程的信息等

@property (class, readonly, strong) NSThread *mainThread;//獲取到主線程
@property (class, readonly, strong) NSThread *currentThread;//獲取當前線程
@property (class, readonly) BOOL isMainThread;//當前是否是主線程
+ (BOOL)isMultiThreaded;//是否是多線程
+ (void)sleepUntilDate:(NSDate *)date;//當前線程休眠到某時刻
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;//當前線程休眠時長
+ (void)exit;//退出當前線程
+ (double)threadPriority;//當前線程的優(yōu)先級
+ (BOOL)setThreadPriority:(double)p;//設置當前線程的優(yōu)先級

示例

我們修改一下任務run的實現(xiàn)

-(void)run:(id)object{
    // 獲取主線程
    NSThread *mainThread = [NSThread mainThread];
    mainThread.name = @"主線程";
    NSLog(@"主線程:%@",mainThread);
    // 獲取當前的線程,可以通過這個線程實例使用線程方法
    NSThread *currentThread = [NSThread currentThread];
    currentThread.name = @"子線程";
    NSLog(@"當前線程:%@",currentThread);
    // 線程的優(yōu)先級
    [NSThread setThreadPriority:0.8];
    NSLog(@"%f-%f",[NSThread threadPriority],currentThread.threadPriority);
    // 線程信息
    NSLog(@"多線程?:%i",[NSThread isMultiThreaded]);
    // 是否是主線程
    NSLog(@"當前線程是否是主線程:%i-%i",[NSThread isMainThread],[currentThread isMainThread]);
    // 線程休眠(阻塞)
    // 休眠時長
    [NSThread sleepForTimeInterval:1.0];
    // 休眠到某個時間點
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
    // 終止線程,一旦執(zhí)行exit,線程之后的任務不再被執(zhí)行
    [NSThread exit];
}

小結(jié)

在某一線程任務中(包括主線程)使用NSThread的類方法時,都是針對某一線程的,如設置優(yōu)先級、阻塞、終止等

線程通信

截止到現(xiàn)在,我們已經(jīng)知曉了如何創(chuàng)建線程,管理線程,傳遞參數(shù)等等,但是讀者仔細觀察一下NSThread的方法,其中并沒有為線程賦予任務的方法,這意味著我們通過[[NSThread alloc] init]、[NSThread new]的線程無法指定其任務,那么如果遇到一個需求:耗時的操作開辟子線程去完成,完成后進行刷新UI,這樣的創(chuàng)建線程能否完成呢?我們知道UI需要在主線程中去完成的,那么為了完成上述需求,我們勢必要在子線程中取到主線程,讓其完成UI的更新,但是,正如之前提到的,NSThread并沒有為線程賦予任務的方法,并且當前線程是只讀屬性,無法更換為主線程,因此,這種需求顯然是無法實現(xiàn)的。(筆者試過取消當前線程,調(diào)用更新UI;還是創(chuàng)建block作為參數(shù),回調(diào)結(jié)果更新UI,最終發(fā)現(xiàn)當前線程都是子線程,這說明消息傳遞默認都是在當前線程上完成的)

那么,NSThread無法完成了上述的需求了嗎?答案是否定的,在NSThread的最后,apple擴展了NSObject對象,這些方法,是從實例對象角度,選擇線程執(zhí)行任務的,之前的NSThread都是從線程角度完成任務。

// 在主線程完成任務
// waitUntilDone:表示是否阻塞當前線程等待新任務結(jié)束(結(jié)束后會繼續(xù)執(zhí)行后面任務)
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
// 相當于第一種方法使用kCFRunLoopCommonModes模式
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// 選擇某一線程完成任務
// waitUntilDone:表示是否阻塞當前線程等待新任務結(jié)束(結(jié)束后會繼續(xù)執(zhí)行后面任務)
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);//相當于第一種方法使用kCFRunLoopCommonModes模式
// 在后臺完成任務
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;

示例

我們來完成上述需求

@interface ViewController (){
    NSInteger _count; // 定義一個計數(shù)器
}
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 開啟一個線程
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}

-(void)run{
    NSLog(@"%s,當前線程:%@",__func__,[NSThread currentThread]);
    _count = 1;
    NSLog(@"UI更新前的count:%ld",_count);
    // 在主線程上更新UI,這里只做計算器加1
    // 注:這里的waitUntilDone是YES,表示等待新任務結(jié)束后再繼續(xù)執(zhí)行
    [self performSelectorOnMainThread:@selector(updateUI:) withObject:@(_count) waitUntilDone:YES];
    NSLog(@"UI更新后的count:%ld",_count);
}
-(void)updateUI:(NSNumber*)count{
    NSLog(@"%s,當前線程:%@",__func__,[NSThread currentThread]);
    _count++;
}

結(jié)果

結(jié)果

我們發(fā)現(xiàn),updateUI中的線程是主線程,而不是子線程,另外我們發(fā)現(xiàn),count變成了2,說明run中的任務是順序執(zhí)行的。

為了對比waitUntilDone的效果,我們將其改為NO


結(jié)果

我們發(fā)現(xiàn),count依舊輸出了兩次,但是數(shù)量都是1,這說明run中的任務并沒有順序執(zhí)行,因此waitUntilDone為YES還是NO需要根據(jù)需求而定。

另外需要注意的是,此時updateUI的主線程任務是加到主線程任務最后才被執(zhí)行的,先來先服務的原則。

為了對比NSThreadPerformAdditions擴展調(diào)用方法和普通的訪問的不同,我們將

[self performSelectorOnMainThread:@selector(updateUI:) withObject:@(_count) waitUntilDone:NO];

改為

[self updateUI:@(_count)];

結(jié)果:

結(jié)果

我們發(fā)現(xiàn)結(jié)果似乎和第一次一樣,但是,需要注意的是,<NSThread: 0x1700798c0>{number = 3, name = (null)},這里,這說明線程依舊是原來的子線程(number為1時,才是主線程,因為number是遞增的,主線程最先被創(chuàng)建出來,其值為1)

那么,這種調(diào)用方式似乎等同于下面的方法

[self performSelector:@selector(updateUI:) onThread:[NSThread currentThread] withObject:@(_count) waitUntilDone:YES];

結(jié)果:

結(jié)果

結(jié)果一致,[object method]這種方式都是在當前線程上執(zhí)行的。

類比拓展:延遲執(zhí)行

[self performSelector:@selector(updateUI:) withObject:@(_count) afterDelay:1.0];

等同于

[NSThread sleepForTimeInterval:1.0];
[self performSelector:@selector(updateUI:) onThread:[NSThread currentThread] withObject:@(_count) waitUntilDone:YES];

總結(jié)

1、我們可以創(chuàng)建子線程完成一些其他任務
2、NSThread提供了一些類方法,讓我們可以便捷的創(chuàng)建使用子線程
3、NSThread的對象都是在創(chuàng)建時指定了線程任務,并未提供給線程賦予執(zhí)行新任務的方式(因此即使你在子線程中拿到了主線程對象,你也無法執(zhí)行主線程完成一些任務)
4、NSObject擴展了一些方法,用來指定在某些線程上完成任務
5、子線程執(zhí)行耗時操作,主線程更新UI的任務需求
6、[object method]這種調(diào)用方式是以當前線程來執(zhí)行的


NSOperation和NSOperationQueue簡介

NSOperation:

即操作對象,是一個抽象類,用于封裝和單個任務關(guān)聯(lián)的代碼和數(shù)據(jù)。因為它是抽象的,所以不直接使用這個類,而是使用子類(NSInvocationOperation或NSBlockOperation)或使用自定義的子類之一來執(zhí)行實際任務。
盡管NSOperation是抽象類,但其基本實現(xiàn)重要的邏輯來協(xié)調(diào)我們?nèi)蝿詹踩珗?zhí)行,這種內(nèi)置的邏輯使得我們可以專注于我們?nèi)蝿盏膶崿F(xiàn),而不是和其他事務邏輯耦合。
NSOperation是一次性對象,即它只會執(zhí)行一次任務,不能再使用它來執(zhí)行其他任務。我們通常將操作對象添加到操作隊列(NSOperationQueue類的實例)來同步或異步的執(zhí)行操作,除了在操作隊列上使用,我們還可以開子線程運行他們或使用結(jié)合GCD來執(zhí)行操作,此時這些操作任務會在當前線程上執(zhí)行。
如果將NSOperation對象添加到NSOperationQueue隊列上,操作對象們會被操作隊列管理開啟操作,如果單獨使用操作隊列,則需要直接調(diào)用其start方法來開啟任務。手動執(zhí)行操作會給代碼帶來更多的負擔,因為啟動未處于isReady狀態(tài)的操作會觸發(fā)異常。

NSOperationQueue:

NSOperationQueue類是管理調(diào)用一組NSOperation對象的類。當操作對象加入到隊列,該操作直到被明確cancelledfinished 前都會被保留在隊列中。操作隊列中的操作本身根據(jù)優(yōu)先級和其他操作項的對象依賴性進行組織,并相應的執(zhí)行。應用程序可能會創(chuàng)建多個操作隊列并將操作提交給它們中的任何一個。
即使這些操作對象位于不同的操作隊列中,操作間的依賴關(guān)系也是有效的。操作對象在其所有依賴操作完成執(zhí)行之前不會進入isReady就緒狀態(tài)。對于isReady狀態(tài)的操作對象,最高優(yōu)先級的操作的對象將會被優(yōu)先執(zhí)行。
對于已加入到操作隊列里的操作對象,我們無法將其移除,操作隊列會保持在隊列中,直到其完成其任務,當然,這里的完成是一種狀態(tài),并不一定意味著真正的完成了其任務,因為操作對象在未被執(zhí)行前,是可以取消的。取消操作對象依舊被保留在隊列中,但是會通知對象盡快終止其任務。對于當前正在執(zhí)行的操作,這意味著操作對象的工作代碼必須檢查取消狀態(tài),停止正在執(zhí)行的操作,并將自己標記為finished已完成狀態(tài)。對于'isReady'狀態(tài)但是尚未執(zhí)行的操作,隊列仍必須調(diào)用該操作對象的start方法,以便它可以處理取消事件并將自己標記為finished已完成狀態(tài)。
取消操作會導致操作忽略一切和它具有依賴關(guān)系。這意味著和其有依賴關(guān)系的操作可能更早的被執(zhí)行。
和NSOperation一樣,操作隊列通常會在當前線程。另外,操作隊列會使用GCD來啟動其操作對象的執(zhí)行,因此,操作對象總是在單獨的線程上執(zhí)行,而不管它們是否被指定為異步或同步操作。

上面是官方給出的解釋,總體而言,NSOperation是操作對象類,NSOperationQueue是操作對象的調(diào)度者。

NSOperation類介紹

NSOperation類是一個抽象類,該類并沒賦予其完成任務的能力,我們可以使用它的兩個子類NSBlockOperationNSInvocationOperation,前者是通過block的形式完成任務,后者是則使用方法選擇器實現(xiàn)。另外,我們還可以自定義自己的操作類。

  • NSBlockOperation

NSBlockOperation用于管理一個或多個block任務的并發(fā)執(zhí)行,我們可以使用該對象實現(xiàn)一次執(zhí)行多個block任務,而無需為每個block任務創(chuàng)建單獨的操作對象。當執(zhí)行多個block任務時,只有當所有的block完成時才認為操作本身已完成。

注:添加到block的任務是以默認優(yōu)先級調(diào)度到隊列的,并且block任務內(nèi)部不可以在去更改執(zhí)行環(huán)境。

-(void)useBlockOperation{
    // 創(chuàng)建操作對象
    NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
    }];
    // 單獨使用操作對象時需要手動開啟任務
    // 啟動
    [bop start];
}

當我們在主線程中測試時:

 [self useBlockOperation];
主線程

當我們在某線程中測試時:

[NSThread detachNewThreadSelector:@selector(useBlockOperation) toTarget:self withObject:nil];
子線程

上面兩個測試可知:操作對象會在給定的線程中執(zhí)行任務。

NSBlockOperation對象可以添加更多的block任務,并在主線程中調(diào)用

-(void)useBlockOperation{
    // 創(chuàng)建操作對象
    NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"origin:%@",[NSThread currentThread]);
    }];
    for (int i=0; i<10; i++) {
        // 添加更多的額外任務
        [bop addExecutionBlock:^{
            NSLog(@"%@:%@",[NSString stringWithFormat:@"%i",i],[NSThread currentThread]);
        }];
        NSLog(@"操作任務是否完成:%@",bop.isFinished?@"YES":@"NO");
    }
    // 啟動
    [bop start];
    NSLog(@"操作任務是否完成:%@",bop.isFinished?@"YES":@"NO");
}
新增任務

我們發(fā)現(xiàn),block中的操作任務是并發(fā)執(zhí)行的,系統(tǒng)會在開啟一個或多個線程執(zhí)行這些任務,可以看到任務是在1和3號線程執(zhí)行的,其中1號線程是主線程,因為我們是在主線程中調(diào)用操作對象的。如果我們在子線程中調(diào)用,則不會出現(xiàn)主線程,而會繼續(xù)創(chuàng)建子線程去執(zhí)行任務。另外,我們發(fā)現(xiàn),只有當所有的block完成時,操作本身才會被標記為已完成狀態(tài)。

  • NSInvocationOperation

NSInvocationOperation對象用于指定調(diào)用者的任務。我們可以通過某個對象開啟一個操作,并指定對象調(diào)用選擇器執(zhí)行任務。

[NSThread detachNewThreadSelector:@selector(useInvocationOperation) toTarget:self withObject:nil];

// invocation操作對象
-(void)useInvocationOperation{
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperation:) object:@{@"name":@"NSInvocationOperation"}];
    [op start];
}
// 任務
-(void)invocationOperation:(NSDictionary*)dic{
    NSLog(@"%@\n%@",[NSThread currentThread],dic);
}
子線程

我們看到NSInvocationOperation在新開啟的線程被執(zhí)行。

  • 自定義操作類

NSOperationQueue 操作隊列

對于NSOperation類,我們發(fā)現(xiàn),NSOperation的對象執(zhí)行任務時,除了BlockOperation中的addExecutionBlock開啟了新的線程,似乎和我們普通對象調(diào)用方法沒什么不同,都是在當前線程執(zhí)行任務,想要實現(xiàn)多線程編程,都需要借助GCD或NSTread來開啟子線程,那這個操作類是否顯得非常雞肋呢?當然不是,其實NSOperation是需要結(jié)合NSOperationQueue才能發(fā)揮其作用的,NSOperationQueue是基于GCD的封裝對象,用來便捷的完成NSOperation對象執(zhí)行操作事務的調(diào)度工作。

NSOperationQueue 可以調(diào)度 NSOperation 對象,隊列中的操作對象都是并發(fā)執(zhí)行。

-(void)useOperationQueue{
    NSLog(@"queue:%@",[NSThread currentThread]);
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    for (int i=0; i<3; i++) {
        NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"%@:%@",[NSString stringWithFormat:@"%i",i],[NSThread currentThread]);
        }];
        // 添加到操作隊列
        [queue addOperation:bop];
    }
    // 或者快捷添加操作對象
    [queue addOperationWithBlock:^{
        NSLog(@"Add directly:%@",[NSThread currentThread]);
    }];
}
queue隊列

我們發(fā)現(xiàn),隊列雖然是在主線程中執(zhí)行,但是操作對象都是運行在子線程中,并且所有的操作對象都不需要手動開啟,都是由隊列自動調(diào)度開啟。

關(guān)于操作對象的執(zhí)行順序會根據(jù)當前處于isReady狀態(tài)的操作對象的優(yōu)先級調(diào)用,另外,設置依賴關(guān)系的操作對象會在依賴對象完成后進入isReady狀態(tài),換句話說,被依賴對象優(yōu)先于依賴對象執(zhí)行。

首先我們先創(chuàng)建兩個操作對象

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *bop = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"block:%@",[NSThread currentThread]);
}];
[bop addExecutionBlock:^{
    NSLog(@"blockNew:%@",[NSThread currentThread]);
}];
[bop addExecutionBlock:^{
    NSLog(@"blockNew2:%@",[NSThread currentThread]);
}];
NSInvocationOperation *iop = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperation:) object:nil];
[queue addOperations:@[iop,bop] waitUntilFinished:NO];
沒有依賴關(guān)系的

我們發(fā)現(xiàn)這兩個操作對象的任務都是并發(fā)的。

  • 依賴

我們?yōu)閮蓚€操作項添加依賴。

// 添加依賴關(guān)系
[iop addDependency:bop];
添加依賴

我們發(fā)現(xiàn),由于iop依賴bop,所以iop始終會等到bop完成后才被執(zhí)行。

添加依賴時,不要出現(xiàn)循環(huán)依賴的情況,會導致死鎖,死鎖的兩個操作對象都等待對方完成,結(jié)果都不能執(zhí)行。

  • 死鎖

另外,如果A依賴B,B手動先于A執(zhí)行,系統(tǒng)則會拋出異常。

[iop addDependency:bop];
[iop start];
異常操作
  • 等待所有操作完成

如果我們需要在所有操作任務完成之后執(zhí)行一些事務,則可以設置等待。

[queue addOperations:@[iop,bop] waitUntilFinished:YES];

或者

[queue waitUntilAllOperationsAreFinished];

該方法會阻塞當前線程,等待所有操作對象完成任務。在此期間,當前線程不能再往隊列中添加操作對象,當然,其他線程可以添加操作對象,一旦所有掛起的操作對象完成,此方法返回,如果隊列中沒有操作對象,則此方法立刻返回。

  • 操作隊列數(shù)量
@property NSInteger maxConcurrentOperationCount;

默認為-1,不限制,當設置為1時,相當于一個同步執(zhí)行的操作隊列。

其他的屬性/方法

  • NSOperation

可監(jiān)聽的一些屬性:

// 操作已取消
@property (readonly, getter=isCancelled) BOOL cancelled;
// 操作正在執(zhí)行
@property (readonly, getter=isExecuting) BOOL executing;
// 操作已完成
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below
// 任務是并發(fā)還是同步執(zhí)行,當操作任務加入到操作隊列后,會忽略該屬性
@property (readonly, getter=isAsynchronous) BOOL asynchronous ;
// 任務是否已經(jīng)就緒,當其依賴的操作任務都執(zhí)行完時,改狀態(tài)才會是YES
@property (readonly, getter=isReady) BOOL ready;

添加/移除依賴

- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;

開啟/取消操作任務

- (void)start;
- (void)cancel;

優(yōu)先級

@property NSOperationQueuePriority queuePriority;
// 取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

阻塞當前線程直到操作完成

- (void)waitUntilFinished;

操作完成回調(diào)

@property (nullable, copy) void (^completionBlock)(void);
  • NSOperationQueue

當前操作數(shù)量

@property (readonly) NSUInteger operationCount;

暫停/恢復隊列

@property (getter=isSuspended) BOOL suspended;

取消隊列中所有操作任務

- (void)cancelAllOperations;

當前隊列/主隊列

@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue;
@property (class, readonly, strong) NSOperationQueue *mainQueue;

我們可以獲取到主隊列,再調(diào)用

- (void)addOperationWithBlock:(void (^)(void))block;

例如在主隊列中完成UI的更新。

[[NSOperationQueue mainQueue] addOperationWithBlock:^{
    self.contentImageView.image = image;
}];

GCD簡介

GCD 是 Apple 開發(fā)的一個多核編程的解決方法,簡單易用,效率高,速度快。通過 GCD,開發(fā)者只需要向隊列中添加一段代碼塊,而不需要直接和線程打交道。GCD在后端管理著一個線程池,它不僅決定著你的代碼塊將在哪個線程被執(zhí)行,還根據(jù)可用的系統(tǒng)資源對這些線程進行管理。這樣通過GCD來管理線程,從而解決線程被創(chuàng)建的問題。

GCD基本概念

任務和隊列

名詞 說明
線程 程序執(zhí)行任務的最小調(diào)度單元
任務 一段代碼:GCD中,block中需要執(zhí)行的代碼
隊列 存放任務的數(shù)組,F(xiàn)IFO(先進先出)的原則

異步、同步,并行和串行

名詞 說明
異步(async) 具備開新線程的能力
同步(sync) 不具備開新線程的能力
并行 任務可以并發(fā)執(zhí)行
串行 任務按順序執(zhí)行

說明:使用GCD開啟多線程執(zhí)行多個任務時,需具備兩個條件:

1、能開啟新線程
2、任務可以并發(fā)執(zhí)行

即:"異步"+"并行"

GCD幾種組合

- 并行隊列 串行隊列 主隊列
異步 開啟新線程,任務同時執(zhí)行(1) 開啟新線程,任務順序執(zhí)行(2) 不開新線程,任務順序執(zhí)行(5)
同步 不開啟新線程,任務順序執(zhí)行(3) 不開啟新線程,任務順序執(zhí)行(4) 主線程中:死鎖。子線程中:不開啟新線程,任務順序執(zhí)行(6)

詳細說明:
(1)異步使得隊列開啟了新的線程,并行隊列讓任務可以同時執(zhí)行(常用)
(2)雖然開啟了新線程,但是隊列調(diào)度方式是串行的,因此任務只能順序執(zhí)行
(3)同步意味著不能開啟新線程,雖然是并行隊列,但線程只有一個,因此任務只能順序執(zhí)行
(4)不能開新線程,任務隊列是串行,任務順序執(zhí)行

總結(jié):正如上面說到,使用GCD完成多線程多任務時,需要具備兩個能力:開啟新線程的能力,任務可以同時執(zhí)行的能力,即"異步"+"并行"。兩者缺一不可,缺少任何一個,隊列里的任務都是順序執(zhí)行。

接下來說明一下主隊列:主隊列其實是一個串行隊列

(5)主隊列里的任務都是在主線程中完成的,即使使用異步(async),也不會開啟新線程,并且主隊列是一個串行隊列,任務順序執(zhí)行。
(6)在主線程中出現(xiàn)死鎖,是因為任務被加到主隊列中,想要被執(zhí)行block中的代碼必須等到主線程上的任務都執(zhí)行完畢,但是,因為是同步任務,想要主線程上的任務執(zhí)行完畢,勢必需要執(zhí)行任務中的block中的代碼,因此兩者相互等待,出現(xiàn)死鎖;但是如果在子線程中添加同步任務,并不會阻塞主線程上的任務執(zhí)行完畢,因此結(jié)果會和"同步"+"串行"一致。

GCD的基本使用

GCD使用步驟分兩步:

1、獲取一個隊列
2、將任務添加到隊列中

系統(tǒng)會自動調(diào)度任務,通常是FIFO(先來先服務)

獲取隊列

// DISPATCH_QUEUE_SERIAL 串行
// DISPATCH_QUEUE_CONCURRENT 并行
dispatch_queue_create("隊列標識符",隊列類型);

我們可以使用 dispatch_queue_create 來創(chuàng)建隊列,隊列類型有兩種:DISPATCH_QUEUE_SERIAL串行隊列,DISPATCH_QUEUE_CONCURRENT并行隊列。

GCD為我們提供了兩種快捷獲取隊列的方式,一個是主列隊(串行隊列),一種是全局隊列(并行隊列)

1、獲取主隊列

dispatch_get_main_queue();

主隊列中任務都會放到主線程中執(zhí)行,并且是順序執(zhí)行。

2、獲取全局并發(fā)隊列

dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

第一個參數(shù)是隊列的優(yōu)先級,一般選擇默認即可;
DISPATCH_QUEUE_PRIORITY_HIGH
DISPATCH_QUEUE_PRIORITY_DEFAULT 默認
DISPATCH_QUEUE_PRIORITY_LOW
DISPATCH_QUEUE_PRIORITY_BACKGROUND 后臺

創(chuàng)建任務的方法

GCD為我們提供了創(chuàng)建同步和異步的方法,分別是dispatch_syncdispatch_async,其中第一個參數(shù)是隊列,第二個是需要執(zhí)行的block塊,我們的任務就放在這里

// 創(chuàng)建同步任務
dispatch_sync(queue, ^{
    // 同步任務
});
// 創(chuàng)建異步任務
dispatch_async(queue, ^{
    // 異步任務
});

GCD線程通信

在開發(fā)過程中,我們通常將一些耗時的操作放在子線程,如數(shù)據(jù)請求,文件下載等,當這些任務完成后,我們需要即使的更新到UI上,這時我們就需要回到主線程

//獲取【并行】隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 創(chuàng)建【異步】任務
dispatch_async(queue, ^{
    // do something ...
    [NSThread sleepForTimeInterval:2.0];
    // 回到主線程
    dispatch_queue_t main = dispatch_get_main_queue();
    dispatch_async(main, ^{
        // 更新UI
    });
});

整個過程如上述代碼所示,我們獲取并行隊列,創(chuàng)建異步任務,但完成耗時操作后,再獲取到主線程,將更新UI的任務添加到主線程上去。

dispatch_queue_t main = dispatch_get_main_queue();
dispatch_async(main, ^{
    // 更新UI
});

驗證組合

(1)【異步】+【并行】

NSLog(@"current thread:%@",[NSThread currentThread]);
// 創(chuàng)建【并行】隊列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
// 開啟多個【異步】任務
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");
結(jié)果

我們發(fā)現(xiàn),"end"在線程輸出之前,說明當前線程并未等待,而是開啟了新的線程執(zhí)行任務(3、4、5號線程);任務1、2、3交替完成,說明并發(fā)隊列在同時執(zhí)行多個任務。

(2)【異步】+【串行】

我們將【并行】隊列改為【串行】隊列。

NSLog(@"current thread:%@",[NSThread currentThread]);
// 創(chuàng)建【串行】隊列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
// 開啟多個【異步】任務
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");
結(jié)果

我們發(fā)現(xiàn),雖然開啟了新的線程,但是隊列是一個串行隊列,因此任務是順序執(zhí)行的:1->2->3,另外,我們注意到,串行隊列下,dispatch_async只會開啟一個線程。

(3)【同步】+【并行】

我們再稍稍改動代碼,從而產(chǎn)生并行隊列,同步任務。

NSLog(@"current thread:%@",[NSThread currentThread]);
// 創(chuàng)建【并行】隊列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
// 開啟多個【同步】任務
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");
結(jié)果

我們可以看到,雖然是并行隊列,但是dispatch_sync同步的條件使得任務并沒有開啟新的線程,而是在當前線程(例子中是主線程)執(zhí)行,并且按照順序執(zhí)行,另外,我們可以看到,"end"是在最后才輸出,這就是同步的原因。

(4)【同步】+【串行】

NSLog(@"current thread:%@",[NSThread currentThread]);
// 獲取【串行】隊列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
// 開啟多個【同步】任務
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");
結(jié)果

我們發(fā)現(xiàn),同步+串行和同步+并行的結(jié)果是一致的。

最后,我們來看下,比較特殊的串行隊列的主隊列

(5)【異步】+【主隊列】

NSLog(@"current thread:%@",[NSThread currentThread]);
// 獲取主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
// 開啟多個【異步】任務
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_async(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");
結(jié)果

我們發(fā)現(xiàn),結(jié)果似乎和【異步】+【串行】一樣,其實主隊列就是一種串行隊列,不同的是,主隊列并不會開啟新的線程,所有的任務都會放在主線程中完成,并且服從FIFO先來先服務的原則

(6)【同步】+【主隊列】

NSLog(@"current thread:%@",[NSThread currentThread]);
// 獲取主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
// 開啟多個【同步】任務
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
dispatch_sync(queue, ^{
    for (int i=0; i<2; i++) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"3:%@",[NSThread currentThread]);
    }
});
NSLog(@"end");
結(jié)果

呃呃。。。我們發(fā)現(xiàn),在主線程中,【同步】+【主隊列】的方式發(fā)生了死鎖,這個是因為dispatch_sync將任務添加到主隊列中,任務block部分需要等待主線程上的任務執(zhí)行完畢之后才會執(zhí)行,但是由于dispatch_sync會阻塞當前線程,直到之前的任務都完成才會繼續(xù)執(zhí)行,這導致主線程的任務永不能完成,任務block里的代碼也用不能被執(zhí)行,從而產(chǎn)生了死鎖。

既然dispatch_sync會阻塞當前線程,那我們將其放在子線程中會怎么樣呢?

我們開啟一個子線程測試

[NSThread detachNewThreadSelector:@selector(mainThreadVSSync) toTarget:self withObject:nil];

在子線程任務中,重新執(zhí)行【同步】+【主隊列】任務

-(void)mainThreadVSSync{
    NSLog(@"current thread:%@",[NSThread currentThread]);
    // 獲取主隊列
    dispatch_queue_t queue = dispatch_get_main_queue();
    // 開啟多個【同步】任務
    dispatch_sync(queue, ^{
        for (int i=0; i<2; i++) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"1:%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
        for (int i=0; i<2; i++) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"2:%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
        for (int i=0; i<2; i++) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"3:%@",[NSThread currentThread]);
        }
    });
    NSLog(@"end");
}
結(jié)果

我們發(fā)現(xiàn),程序可以繼續(xù)執(zhí)行。我們使用【同步】+【主隊列】時,線程不在是主線程,而是3號線程,這樣dispatch_sync開啟同步任務時,并不會影響到主線程,同步任務可以繼續(xù)執(zhí)行,只不過都是在主線程中,而且是順序執(zhí)行。

總結(jié)

1、GCD方式實現(xiàn)多線程多任務必須是【異步】+【并行隊列】,缺一不可,其他方式的任務都是順序執(zhí)行的,無論異步還是同步。

2、主隊列是一種串行隊列,切忌在主線程中使用【同步】+【主隊列】的方式開啟任務,會出現(xiàn)死鎖。

GCD其他

隊列組:dispatch_group和dispatch_group_notify

有時,我們需要開啟多個異步任務,并且所有任務都結(jié)束之后,再回到主線程執(zhí)行任務,那么該如何做呢?這里我們就需要dispatch_group了。

步驟:創(chuàng)建group->關(guān)聯(lián)任務、隊列、group->接受group通知

a、我們通過dispatch_group_create()創(chuàng)建一個隊列組

b、使用dispatch_group_async方法,將任務放在隊列中,隊列則關(guān)聯(lián)到group

c、接收完成通知dispatch_group_notify

// 創(chuàng)建一個隊列組
dispatch_group_t group = dispatch_group_create();
// 獲取一個【并行】隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 使用隊列組發(fā)起一個耗時任務
dispatch_group_async(group, queue, ^{
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"1:%@",[NSThread currentThread]);
    }
});
// 另外一個耗時任務
dispatch_group_async(group, queue, ^{
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"2:%@",[NSThread currentThread]);
    }
});
// 完成通知,在主隊列中完成UI更新
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"更新UI操作");
    NSLog(@"%@",[NSThread currentThread]);
});
結(jié)果

我們可以看到,只有當多線程中多任務完成后,dispatch_group_notify中更新UI的操作才會被執(zhí)行

dispatch_group_wait

當方法會阻塞當前線程,等待指定當group中當任務執(zhí)行完成后,才會繼續(xù)執(zhí)行。

1中的例子,也可以使用該方法達到同樣的效果

    // 創(chuàng)建一個隊列組
    dispatch_group_t group = dispatch_group_create();
    // 獲取一個【并行】隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 使用隊列組發(fā)起一個耗時任務
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"1:%@",[NSThread currentThread]);
        }
    });
    // 另外一個耗時任務
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"2:%@",[NSThread currentThread]);
        }
    });
    // 阻塞當前線程
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"更新UI操作");
    NSLog(@"%@",[NSThread currentThread]);
結(jié)果

但是,需要注意的是,dispatch_group_wait是會阻塞當前線程的,而dispatch_group_notify則不會。

延遲執(zhí)行:dispatch_after

我們可以使用GCD快速的創(chuàng)建一個延遲執(zhí)行的任務。當然,由于是添加到主隊列的中的,因此這個延遲的時間是不準確的,這里還包括了隊列前的任務時長。

NSLog(@"執(zhí)行前:%@",[NSThread currentThread]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"執(zhí)行時:%@",[NSThread currentThread]);
});
結(jié)果

我們發(fā)現(xiàn),時差為2.2的左右,是大于我們所設的2.0秒的。

一次性代碼:dispatch_once

GCD可以創(chuàng)建一次性代碼,在制作單例時,我們常常使用到它dispatch_once,該函數(shù)可以保證某段代碼在程序中只執(zhí)行1次,并且在多線程的環(huán)境下,也可以保證線程安全。

-(void)onceTask{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 一次性任務
    });
}
快速迭代方法:dispatch_apply

通常我們會使用for循環(huán)遍歷數(shù)組,GCD中的dispatch_apply提供了類似的方法,不同的是,dispatch_apply不僅可以是順序的遍歷,還可以是并發(fā)的遍歷,主要看隊列是串行的還是并行的。

NSLog(@"apply---begin");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
    NSLog(@"%zd---%@",index, [NSThread currentThread]);
});
NSLog(@"apply---end");
并行迭代

我們發(fā)現(xiàn)遍歷并不是順序執(zhí)行的,如果使用的誰串行隊列,和使用for循環(huán)遍歷是一樣的效果。

信號量:dispatch_semaphore

信號量類似生活當中的信號燈,紅燈停,綠燈行。GCD中的信號量Dispatch Semaphore是持有計數(shù)的信號,計數(shù)為0時等待,不可通行,計數(shù)為1或者大于1時,計數(shù)減1且允許通過。

dispatch_semaphore 有三個函數(shù),分別用來創(chuàng)建信號量,增加計數(shù)量

dispatch_semaphore_create:創(chuàng)建并初始化信號的總量
dispatch_semaphore_signal:發(fā)送一個信號,讓信號總量加1
dispatch_semaphore_wait:可以使總信號量減1,當信號總量為0時就會一直等待(阻塞所在線程),否則就可以正常執(zhí)行

使用信號量時,需要清楚地分清等待和執(zhí)行的線程。

應用:

1、保持線程同步,將異步執(zhí)行的任務轉(zhuǎn)為同步執(zhí)行任務
2、保證線程安全,為線程加鎖

應用一:異步轉(zhuǎn)同步

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1.0];
    NSLog(@"1:%@",[NSThread currentThread]);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore end");
結(jié)果

我們在當前線程中開啟了一個異步并行隊列任務,由于我們使用了信號量semaphore(其初始值為0,不可通行),當我們使用dispatch_semaphore_wait,會阻塞當前線程,直到信號量不為0時,才會執(zhí)行NSLog(@"semaphore end")的輸出,上述例子中,并沒有給信號量計數(shù)器加1,因此不會執(zhí)行后面輸出任務。

我們?yōu)槠涮砑蛹右?/p>

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1.0];
    NSLog(@"1:%@",[NSThread currentThread]);
    dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore end");
結(jié)果

此時我們發(fā)現(xiàn),程序正常輸出。另外,我們這里通過通過信號量實現(xiàn)線程的同步操作(輸出在異步任務之后),原本的異步線程在這里并沒有什么效果,和同步任務沒有任何區(qū)別

應用二:線程安全

結(jié)合GCD的信號量的特性,我們還可以使用其達到線程安全的目的,即在多線程下,保證事務的原子性。

假設我分別開啟兩個線程去做加一操作,在不保證線程安全的情況下,勢必會出現(xiàn)線程爭搶資源,導致意想不到的錯誤產(chǎn)生。

首先我們來看下非線程安全。

dispatch_queue_t queue1 = dispatch_queue_create("one", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("two", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue1, ^{
    [self addCount];
});
dispatch_async(queue2, ^{
    [self addCount];
});

-(void)addCount{
    while (1) {
        if (count>=100) {
            break;
        }else{
            count++;
            NSLog(@"%ld-%@",count,[NSThread currentThread]);
        }
    }
}
結(jié)果

我們發(fā)現(xiàn),在此次過程中,線程3和4發(fā)生了資源爭搶的問題,導致在加1的過程中發(fā)生了錯誤,兩個線程累加的次數(shù)總和超過了100次。

接下來,我們使用信號量來保證事務的原子性。

// 創(chuàng)建信號量
semaphore = dispatch_semaphore_create(1);
// 在事務的開始和結(jié)束時操作信號量
-(void)addCount{
    while (1) {
        // 信號量減一,進入等待狀態(tài)
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        if (count>=100) {
            // 信號量加一,進入可通行狀態(tài)
            dispatch_semaphore_signal(semaphore);
            break;
        }else{
            count++;
            NSLog(@"%ld-%@",count,[NSThread currentThread]);
        }
        // 信號量加一,進入可通行狀態(tài)
        dispatch_semaphore_signal(semaphore);
    }
}
結(jié)果

我們發(fā)現(xiàn),線程3和4交替操作累加,數(shù)字累計達到100時,操作總和為100,因此我們可以認定,信號量起到了很好的效果,保證了事務的原子性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容