iOS多線程使用總結

iOS多線程使用總結

網絡圖片

一.概述與實現方案

1. 線程與進程

多線程在iOS中有著舉足輕重的地位,是每一位開發者都必備的技能,當然也是面試常考的技術點,本文主要是探究我們實際開發或者面試中遇到的多線程問題。比如什么是線程?它跟進程是什么關系,隊列跟線程什么關系,同步、異步、并發(并行)、串行這些概念又怎么來理解,iOS有哪些常用多線程方案,以及線程同步技術有哪些等等。

線程(英語:thread)是操作系統能夠進行運算調度的最小單位。大部分情況下,它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發多個線程,每條線程并行執行不同的任務。 --- 維基百科

這里又多了一個 進程,那什么是進程呢,說白了就是是指在操作系統中正在運行的一個應用程序,如微信、支付寶app等都是一個進程。線程是就是進程的基本執行單元,一個進程的所有任務都在線程中執行。也就是說 一個進程最少要有一個線程,這個線程就是主線程。當然在我們實際使用過程中不可能只有一條主線程,我們為提高程序的執行效率,往往需要開辟多條子線程去執行一些耗時任務,這里就引出了多線程的概念。

多線程(英語:multithreading),是指從軟件或者硬件上實現多個線程并發執行的技術

根據操作系統與硬件的不同分為兩類:軟件多線程硬件多線程

  • 軟件多線程: 即便CPU只能運行一個線程,操作系統也可以通過快速的在不同線程之間進行切換,由于時間間隔很小,來給用戶造成一種多個線程同時運行的假象

  • 硬件多線程: 如果CPU有多個核心,操作系統可以讓每個核心執行一條線程,從而具有真正的同時執行多個線程的能力,當然由于任務數量遠遠多于CPU的核心數量,所以,操作系統也會自動把很多任務輪流調度到每個核心上執行。
    以上都是google出來的一大堆東西,比較抽象,沒關系我們來看下我們實際iOS開發中用到的多線程技術。

2.iOS中的多線程方案

iOS 中的多線程方案主要有四種 PThreadNSThreadGCDNSOperationPThread 是一套純粹C語言的API,能適用于Unix\Linux\Windows等系統,線程生命周期需要程序員自己管理,使用難度較大,在我們的實際開發中幾乎用不到,在這里我們不做過多介紹,感興趣的直接去百度。我們著重介紹另外三中方案。

這里解釋一下線程的生命周期,所謂的線程的生命周期就是線程從創建到死亡的過程。一般會經歷:新建 - 就緒 - 運行 - 阻塞 - 死亡的過程。

  • 新建:就是初始化線程對象
  • 就緒:向線程對象發送start消息,線程對象被加入可調度線程池等待CPU調度。
  • 運行:CPU 負責調度可調度線程池中線程的執行,線程執行完成之前,狀態可能會在就緒和運行之間來回切換。就緒和運行之間的狀態變化由CPU負責,程序員不能干預。
  • 阻塞:當滿足某個預定條件時,可以使用休眠或鎖,阻塞線程執行
  • 死亡:線程執行完畢,退出,銷毀。
(1) NSThread

NSThread是蘋果官方提供面向對象操作線程的技術,簡單方便,可以直接操作線程對象,不過需要自己控制線程的生命周期,我們看下蘋果官方給出的方法。

[1] 初始化方法
  • 實例初始化方法
- (instancetype)init API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

對應的初始化方法:

 //創建線程
 NSThread *newThread = [[NSThread alloc]initWithTarget:self selector:@selector(demo:) object:@"Thread"];
 NSThread *newThread = [[NSThread alloc]init];
 NSThread *newThread = [[NSThread alloc]initWithBlock:^{
     NSLog(@"Block");
 }];

注意三種方法創建完成后都需要執行 [newThread start] 去啟動線程。

  • 類初始化方法
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

注意這兩個類方法創建后就可執行,不需手動開啟

[2] 取消退出

既然有了創建,那就得有退出

// 實例方法 取消線程
- (void)cancel;
//類方法 退出
+ (void)exit;
[3] 線程執行狀態
// 線程正在執行
@property (readonly, getter=isExecuting) BOOL executing API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
// 線程執行結束
@property (readonly, getter=isFinished) BOOL finished API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
// 線程是否可取消
@property (readonly, getter=isCancelled) BOOL cancelled API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
[4] 線程間的通信方法
@interface NSObject (NSThreadPerformAdditions)
/*
* 去主線程執行指定方法
* aSelector: 方法
* arg: 參數
* wait:表示是否等待主線程做完事情后往下走,YES表示做完后執行下面事情,NO表示跟下面事情一起執行
*/
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
/*
* 去指定線程執行指定方法
* aSelector: 方法
* arg: 參數
* wait:表示是否等待本線程做完事情后往下走,YES表示做完后執行下面事,NO表示跟下面事一起執行
*/
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
/*
* 去開啟的子線程執行指定方法
* SEL: 方法
* arg: 參數
*/
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

我們常說的線程間的通信所用的方法其實就是上面的這幾個方法,所有繼承NSObject實例化對象都可調用。當然還有其他方法也可以實現線程間的通信,如:GCDNSOperationNSMachPort端口等形式,我們后面用到在做介紹。
舉個簡單的例子:我們在子線程中下載圖片,然后去主線程展示:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 子線程執行下載方法
    [self performSelectorInBackground:@selector(download) withObject:nil];
}
- (void)download{
    //圖片的網絡路徑
     NSURL *url = [NSURL URLWithString:@"https://p3.ssl.qhimg.com/t011e94f0b9ed8e66b0.png"];
     //下載圖片數據
     NSData *data = [NSData dataWithContentsOfURL:url];
     //生成圖片
     UIImage *image = [UIImage imageWithData:data];
    // 回主線程顯示圖片
    [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
}
- (void)showImage:(UIImage *)image{
    self.imageView.image = image;
}
[5] 其他常用方法
  • +(void)currentThread 獲取當前線程
  • +(BOOL)isMultiThreaded 判斷當前是否運行在子線程
  • -(BOOL)isMainThread 判斷是否在主線程
  • +(void)sleepUntilDate:(NSDate *)date;+ (void)sleepForTimeInterval:(NSTimeInterval)ti; 當前線程休眠時間
(3). GCD

在介紹GCD前我們先來了解下多線程中比較容易混淆的幾個概念

[1]. 同步、異步、并發(并行)、串行
  • 同步和異步主要影響:能不能開啟新的線程
    同步:在當前線程中執行任務,不具備開啟新線程的能力
    異步:在新的線程中執行任務,具備開啟新線程的能力

  • 并發和串行主要影響:任務的執行方式
    并發:也叫并行,也叫并行隊列,多個任務并發(同時)執行
    串行:也叫串行隊列,一個任務執行完畢后,再執行下一個任務

單純的介紹概念比較抽象,我們還是結合實際使用來說明:

[2] GCD 中的同步、異步方法
  • 同步執行方法:dispatch_sync()
  • 異步執行方法:dispatch_async()
    使用方法:
dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

可以看到這個兩個方法需要兩個參數,第一個參數需要傳入一個dispatch_queue_t 類型的隊列,第二個是執行的block。下面介紹一下GCD的隊列

[3] GCD 中的隊列

GCD中的隊列有三種:串行隊列、并行隊列、主隊列,創建方式也非常簡單:

  • 串行隊列
dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);

第一個參數是隊列名稱,第二個是一個宏定義,常用的兩個宏 DISPATCH_QUEUE_SERIALDISPATCH_QUEUE_CONCURRENT分別表示串行隊列和并行隊列,除此之外,宏DISPATCH_QUEUE_SERIAL_INACTIVEDISPATCH_QUEUE_CONCURRENT_INACTIVE 分別表示初始化的串行隊列和并行隊列處于不可活動狀態。看下它的底層實現

dispatch_queue_attr_t
dispatch_queue_attr_make_initially_inactive(
        dispatch_queue_attr_t _Nullable attr);
        
#define DISPATCH_QUEUE_SERIAL_INACTIVE \
        dispatch_queue_attr_make_initially_inactive(DISPATCH_QUEUE_SERIAL)

#define DISPATCH_QUEUE_CONCURRENT_INACTIVE \
        dispatch_queue_attr_make_initially_inactive(DISPATCH_QUEUE_CONCURRENT)

應當注意的是,初始化后處于不可活動狀態的隊列,添加到其中的任務要想開始執行,必須先調用 dispatch_activate()函數使其狀態變更為可活動狀態.

  • 并行隊列
    并行隊列有兩種:

    第一種:全局并發隊列創建方法,也是系統為我們創建好的并發隊列,創建方式
/*  - QOS_CLASS_USER_INTERACTIVE
 *  - QOS_CLASS_USER_INITIATED
 *  - QOS_CLASS_DEFAULT
 *  - QOS_CLASS_UTILITY
 *  - QOS_CLASS_BACKGROUND
*/
//dispatch_get_global_queue(intptr_t identifier, uintptr_t flags); 

dispatch_queue_t queue = dispatch_get_global_queue(0,0);

這里有兩個參數,第一個參數標識線程執行優先級,第二個是蘋果保留參數傳參:0 就可以。

第二種:手動創建并發隊列

// 串行執行,第一個參數是名稱 ,第二個是標識:DISPATCH_QUEUE_CONCURRENT,并發隊列標識
dispatch_queue_t queue = dispatch_queue_create("myQueue",DISPATCH_QUEUE_CONCURRENT);
  • 主隊列
    主隊列是一種特殊的串行隊列
dispatch_queue_t queue = dispatch_get_main_queue();

同步、異步以及隊列的組合就可以實現對任務進行多線程編程的需求了。

  1. 同步串行隊列
  dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    for(NSInteger i = 0; i < 10; i++){
        dispatch_sync(queue1, ^{
            NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
        });
    }
//thread == <NSThread: 0x6000011b8880>{number = 1, name = main} i====n

可以看到沒有開啟新的線程,都是在主線程中執行任務,并且是順序執行的

  1. 同步并行隊列
dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    for(NSInteger i = 0; i < 10; i++){
        dispatch_sync(queue1, ^{
            NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
        });
    }
// thread == <NSThread: 0x600001db8a00>{number = 1, name = main} i====n

也是在主線程中順序執行。

  1. 異步串行隊列
dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    for(NSInteger i = 0; i < 10; i++){
        dispatch_async(queue1, ^{
            NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
        });
    }

開啟子線程,順序執行任務

  1. 異步并發隊列
 dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    for(NSInteger i = 0; i < 10; i++){
        dispatch_async(queue1, ^{
            NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
        });
    }
/*
thread == <NSThread: 0x6000024f9440>{number = 4, name = (null)} i====0
thread == <NSThread: 0x6000024f5340>{number = 5, name = (null)} i====2
thread == <NSThread: 0x6000024a8780>{number = 3, name = (null)} i====3
thread == <NSThread: 0x6000024ac6c0>{number = 6, name = (null)} i====1
thread == <NSThread: 0x6000024f4a80>{number = 8, name = (null)} i====5
thread == <NSThread: 0x6000024b0b40>{number = 7, name = (null)} i====4
thread == <NSThread: 0x60000249cd00>{number = 9, name = (null)} i====6
thread == <NSThread: 0x6000024b0980>{number = 10, name = (null)} i====7
thread == <NSThread: 0x6000024cb900>{number = 11, name = (null)} i====8
thread == <NSThread: 0x6000024f5340>{number = 5, name = (null)} i====9
*/

開啟了多個子線程,并且是并發執行任務。

注意 dispatch_async()具備開辟新線程的能力,但是不表示使用它就一定會開辟新的線程。 例如 傳入的 queue 是主隊列,就是在主線程中執行任務,沒有開辟新線程。

  dispatch_queue_t queue1 = dispatch_get_main_queue();
    for(NSInteger i = 0; i < 10; i++){
        sleep(2);
        dispatch_async(queue1, ^{
            NSLog(@"thread == %@ i====%ld",[NSThread currentThread],(long)i);
        });
    }
//thread == <NSThread: 0x600002b24880>{number = 1, name = main} i====n

主隊列是一種特殊的串行隊列,從打印結果看出,這里執行方式是串行,而且沒有開啟新的線程。

具體任務的執行方式可以參考下面的表格


執行方式
[4] dispatch_ group_ t 隊列組

dispatch_group_t是一個比較實用的方法,通過構造一個組的形式,將各個同步或異步提交任務都加入到同一個組中,當所有任務都完成后會收到通知,用于進一步處理.舉個簡單的例子如下:

dispatch_group_t group = dispatch_group_create();

    dispatch_group_async(group, concurrentQueue, ^{
        for (int i = 0; i < 10; I++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    dispatch_group_async(group, dispatch_get_main_queue(), ^{
        for (int i = 0; i < 10; I++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    dispatch_group_async(group, concurrentQueue, ^{
        for (int i = 0; i < 10; I++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
    dispatch_group_notify(group, concurrentQueue, ^{
        NSLog(@"All Task Complete");
    });
[5] diapatch_barrier_async 柵欄異步調用函數

有異步調用就也有同步調用函數diapatch_barrier_sync(),兩者的區別:dispatch_barrier_sync 需要等待柵欄執行完才會執行柵欄后面的任務,而dispatch_barrier_async 無需等待柵欄執行完,會繼續往下走,有什么用呢?其實柵欄函數用的最多的地方還是實現線程同步使用,比如我們有這樣一個需求:怎么樣利用GCD實現多讀單寫文件的IO操作?也就是怎么樣實現多讀單寫,看代碼:

@interface UserCenter()
{
    // 定義一個并發隊列
    dispatch_queue_t concurrent_queue;
    
    // 用戶數據中心, 可能多個線程需要數據訪問
    NSMutableDictionary *userCenterDic;
}

// 多讀單寫模型
@implementation UserCenter

- (id)init
{
    self = [super init];
    if (self) {
        // 通過宏定義 DISPATCH_QUEUE_CONCURRENT 創建一個并發隊列
        concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        // 創建數據容器
        userCenterDic = [NSMutableDictionary dictionary];
    }
    
    return self;
}

- (id)objectForKey:(NSString *)key
{
    __block id obj;
    // 同步讀取指定數據,立刻返回讀取結果
    dispatch_sync(concurrent_queue, ^{
        obj = [userCenterDic objectForKey:key];
    });
    
    return obj;
}

- (void)setObject:(id)obj forKey:(NSString *)key
{
    // 異步柵欄調用設置數據
    dispatch_barrier_async(concurrent_queue, ^{
        [userCenterDic setObject:obj forKey:key];
    });
}

可以看到把寫操作放入柵欄函數,可以實現線程同步效果
注意:使用dispatch_barrier_async ,該函數只能搭配自定義并發隊列 dispatch_queue_t 使用。不能使用全局并發隊列: dispatch_get_global_queue,否則 dispatch_barrier_async無作用。

[6] 線程死鎖

先來看兩個例子:

dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"執行任務2");
    });// 往主線程里面 同步添加任務 會發生死鎖現象

 dispatch_queue_t myQueue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(myQueue, ^{
        NSLog(@"1111,thread====%@",[NSThread currentThread]);
        
        dispatch_sync(myQueue, ^{
            NSLog(@"2222,thread====%@",[NSThread currentThread]);
        });
    });
// 1111,thread====<NSThread: 0x6000022dd880>{number = 5, name = (null)} 
// crash

上面的例子可以看出,不能向當前的串行隊列,同步添加任務,否則會產生死鎖導致crash。線程死鎖的條件:使用sync函數往當前串行隊列里面添加任務,會產生死鎖。

(4). NSOperation

NSOperation 是蘋果對GCD面向對象的封裝,它的底層是基于GCD實現的,相比于GCD它添加了更多實用的功能

  • 可以添加任務依賴
  • 任務執行狀態的控制
  • 設置最大并發數
    它有兩個核心類分別是NSOperationNSOperationQueue,NSOperation就是對任務進行的封裝,封裝好的任務交給不同的NSOperationQueue即可進行串行隊列的執行或并發隊列的執行。
[1] NSOperation

NSOperation 是一個抽象類,并不能直接實用,必須使用它的子類,有三種方式:NSInvocationOperationNSBlockOperation自定義子類繼承NSOperation,前兩中是蘋果為我們封裝好的,可以直接使用,自定義子類,需要我們實現相應的方法。

  • NSBlockOperation & NSInvocationOperation
    使用:
//創建一個NSBlockOperation對象,傳入一個block
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 5; I++)
    {
        NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
    }
}];

/*
創建一個NSInvocationOperation對象,指定執行的對象和方法
該方法可以接收一個參數即object
*/
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task:) object:@"Hello, World!"];

// 執行
[operation start];
[invocationOperation start];

// 打印: Task1 <NSThread: 0x6000019581c0>{number = 1, name = main} 0

可以看到創建這兩個任務對象去執行任務,并沒有開啟新線程。NSBlockOperation 相比 NSInvocationOperation 多了個addExecutionBlock 追加任務的方法,

 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; I++)
        {
            NSLog(@"task1=====%@ %d", [NSThread currentThread], i);
        }
    }];
    
    [operation addExecutionBlock:^{
       
        NSLog(@"task2=====%@",[NSThread currentThread]);
    }];
    
    [operation addExecutionBlock:^{
       
        NSLog(@"task3=====%@",[NSThread currentThread]);
    }];
    
    [operation addExecutionBlock:^{
       
        NSLog(@"task4=====%@",[NSThread currentThread]);
    }];
    
    [operation start];
/*
task3=====<NSThread: 0x600000509840>{number = 6, name = (null)}
task4=====<NSThread: 0x600000530200>{number = 3, name = (null)}
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 0
task2=====<NSThread: 0x600000511680>{number = 5, name = (null)}
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 1
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 2
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 3
task1=====<NSThread: 0x600000558880>{number = 1, name = main} 4
*/

使用addExecutionBlock追加的任務是并發執行的,如果這個操作的任務數大于1那么會開啟子線程并發執行任務,這里追加的任務不一定就是子線程,也有可能是主線程。

[2] NSOperationQueue

NSOperationQueue 有兩種隊列,一個是主隊列通過[NSOperationQueue mainQueue] 獲取,還有一個是自己創建的隊列[[NSOperationQueue alloc] init],它同時具備并發跟串行的能力,可以通過設置最大并發數來決定。

 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; I++)
        {
            NSLog(@"task1=====%@ %d", [NSThread currentThread], i);
        }
    }];
    
    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; I++)
        {
            NSLog(@"Task2===== %@ %d", [NSThread currentThread], i);
        }
    }];
    
    NSOperationQueue *queues = [[NSOperationQueue alloc] init];
    [queues setMaxConcurrentOperationCount:2];//設置最大并發數,如果設置為1則串行執行
    [queues addOperation:operation];
    [queues addOperation:operation2];
/*
Task2===== <NSThread: 0x600000489940>{number = 4, name = (null)} 0
task1=====<NSThread: 0x6000004e15c0>{number = 5, name = (null)} 0
*/

這個例子有兩個任務,如果設置最大并發數為2,則會開辟兩個線程,并發執行這兩個任務。如果設置為1,則會在新的線程中串行執行。

[3] 任務依賴

addDependency可以建立兩個任務之間的依賴關系,如[operation2 addDependency:operation1]; 為任務2依賴任務1,必須等任務1執行完成后才會執行任務2,看個例子

  NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; I++)
        {
            NSLog(@"task1=====%@ %d", [NSThread currentThread], i);
        }
    }];
    
    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; I++)
        {
            NSLog(@"Task2===== %@ %d", [NSThread currentThread], i);
        }
    }];
    
    NSOperationQueue *queues = [[NSOperationQueue alloc] init];
    [queues setMaxConcurrentOperationCount:2];
    //設置任務依賴
    [operation addDependency:operation2];
    
    [queues addOperation:operation];
    [queues addOperation:operation2];
/*
Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 0
Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 1
Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 2
Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 3
Task2===== <NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 4
task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 0
task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 1
task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 2
task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 3
task1=====<NSThread: 0x6000005b5dc0>{number = 6, name = (null)} 4
*/

還是已上面的例子,設置[operation addDependency:operation2];,可以看到任務2完成后才會執行任務1的操作。

[4] 自定義NSOperation

任務執行狀態的控制是相對于自定義的NSOperation子類來說的。對于自定義NSOperation子類有兩種類型:

  1. 重寫main方法
    只重寫operation的main方法,main方法里面寫要執行的任務,系統底層控制變更任務執行完成狀態,以及任務的退出。看個例子
#import "TestOperation.h"

@interface TestOperation ()
@property (nonatomic, copy) id obj;

@end

@implementation TestOperation

- (instancetype)initWithObject:(id)obj{
    if(self = [super init]){
        self.obj = obj;
    }
    return  self;
}

- (void)main{
    NSLog(@"開始執行任務%@ thread===%@",self.obj,[NSThread currentThread]);
}

調用

  TestOperation *operation4 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務4"]];
    [operation4 setCompletionBlock:^{
        NSLog(@"執行完成 thread===%@",[NSThread currentThread]);
    }];
    [operation4 start];
// 打印
開始執行任務我是任務4 thread===<NSThread: 0x6000008d8880>{number = 1, name = main}
執行完成 thread===<NSThread: 0x60000089fa40>{number = 7, name = (null)}

可以看到任務operation的main方法執行是在主線程中的,只是最后完成后的回調setCompletionBlock是異步的,好像沒什么用,別著急,我們把他放入隊列中執行看下,還是上面的例子,加入隊列執行

 NSOperationQueue *queue4 = [[NSOperationQueue alloc] init];
 TestOperation *operation4 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務4"]];
 TestOperation *operation5 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務5"]];
 TestOperation *operation6 = [[TestOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務6"]];

 [queue4 addOperation:operation4];
 [queue4 addOperation:operation5];
 [queue4 addOperation:operation6];
//打印:
開始執行任務我是任務6 thread===<NSThread: 0x600001fc8200>{number = 5, name = (null)}
開始執行任務我是任務4 thread===<NSThread: 0x600001fcc040>{number = 6, name = (null)}
開始執行任務我是任務5 thread===<NSThread: 0x600001fd7c80>{number = 7, name = (null)}

這時候可以看到任務的并發執行了,operation的main方法執行結束后就會調用各自的dealloc方法進行釋放,任務的生命周期結束。如果我們想讓任務4、5、6 倒序執行,可以添加任務依賴

 [operation4 addDependency:operation5];
 [operation5 addDependency:operation6];
// 打印
開始執行任務我是任務6 thread===<NSThread: 0x600003d04680>{number = 6, name = (null)}
開始執行任務我是任務5 thread===<NSThread: 0x600003d04680>{number = 6, name = (null)}
開始執行任務我是任務4 thread===<NSThread: 0x600003d04680>{number = 6, name = (null)}

這樣做貌似是可以的,但是如果我們的operation 中又存在異步任務(如網絡請求),我們想讓網絡任務6請求完后調用任務5,任務5調用成功后調任務4,那該怎么辦呢,我們先賣個關子,我們在第二節多個請求完成后繼續進行下一個請求的方法總結中介紹。

  1. 重寫start方法
    通過重寫main方法可以實現任務的串行執行,如果要讓任務并發執行,就需要重寫start方法。兩者還是有很大區別的:
    如果只是重寫main方法,方法執行完畢,那么整個operation就會從隊列中被移除。如果你是一個自定義的operation并且它是某些類的代理,這些類恰好有異步方法,這時就會找不到代理導致程序出錯了。然而start方法就算執行完畢,它的finish屬性也不會變,因此你可以控制這個operation的生命周期了。然后在任務完成之后手動cancel掉這個operation即可。
@interface TestStartOperation : NSOperation
- (instancetype)initWithObject:(id)obj;
@property (nonatomic, copy) id obj;
@property (nonatomic, assign, getter=isExecuting) BOOL executing;
@property (nonatomic, assign, getter=isFinished) BOOL finished;
@end
@implementation TestStartOperation
@synthesize executing = _executing;
@synthesize finished = _finished;

- (instancetype)initWithObject:(id)obj{
    if(self = [super init]){
        self.obj = obj;
    }
    return  self;
}
- (void)start{
    
    //在任務開始前設置executing為YES,在此之前可能會進行一些初始化操作
    self.executing = YES;
    NSLog(@"開始執行任務%@ thread===%@",self.obj,[NSThread currentThread]);
    /*
    需要在適當的位置判斷外部是否調用了cancel方法
    如果被cancel了需要正確的結束任務
    */
    if (self.isCancelled)
    {
        //任務被取消正確結束前手動設置狀態
        self.executing = NO;
        self.finished = YES;
        return;
    }
    
    NSString *str = @"https://www.360.cn";
    NSURL *url = [NSURL URLWithString:str];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    NSURLSession *session = [NSURLSession sharedSession];
    __weak typeof(self) weakSelf = self;
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
       // NSLog(@"response==%@",response);
        NSLog(@"TASK完成:====%@ thread====%@",weakSelf.obj,[NSThread currentThread]);
        //任務執行完成后手動設置狀態
        weakSelf.executing = NO;
        weakSelf.finished = YES;
    }];
    [task resume];
}
- (void)setExecuting:(BOOL)executing
{
    //手動調用KVO通知
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    //調用KVO通知
    [self didChangeValueForKey:@"isExecuting"];
}
- (BOOL)isExecuting
{
    return _executing;
}
- (void)setFinished:(BOOL)finished
{
    //手動調用KVO通知
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    //調用KVO通知
    [self didChangeValueForKey:@"isFinished"];
}
- (BOOL)isFinished
{
    return _finished;
}
- (BOOL)isAsynchronous
{
    return YES;
}
- (void)dealloc{
    NSLog(@"Dealloc %@",self.obj);
}

執行與結果

NSOperationQueue *queue4 = [[NSOperationQueue alloc] init];
TestStartOperation *operation4 = [[TestStartOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務4"]];
TestStartOperation *operation5 = [[TestStartOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務5"]];
TestStartOperation *operation6 = [[TestStartOperation alloc] initWithObject:[NSString stringWithFormat:@"我是任務6"]];
//設置任務依賴 
[operation4 addDependency:operation5];
[operation5 addDependency:operation6];
[queue4 addOperation:operation4];
[queue4 addOperation:operation5];
[queue4 addOperation:operation6];
/*打印
開始執行任務我是任務6 thread===<NSThread: 0x600002bb8480>{number = 6, name = (null)}
TASK完成:====我是任務6 thread====<NSThread: 0x600002bd4d80>{number = 8, name = (null)}
開始執行任務我是任務5 thread===<NSThread: 0x600002bb0300>{number = 5, name = (null)}
TASK完成:====我是任務5 thread====<NSThread: 0x600002bb0300>{number = 5, name = (null)}
開始執行任務我是任務4 thread===<NSThread: 0x600002bfb080>{number = 7, name = (null)}
TASK完成:====我是任務4 thread====<NSThread: 0x600002bfb080>{number = 7, name = (null)}
2021-06-22 17:57:56.436591+0800 Interview01-打印[15994:9172130] Dealloc 我是任務4
2021-06-22 17:57:56.436690+0800 Interview01-打印[15994:9172130] Dealloc 我是任務5
2021-06-22 17:57:56.436784+0800 Interview01-打印[15994:9172130] Dealloc 我是任務6
*/

在這個例子中我們在任務請求完成后,手動設置其self.executingself.finished狀態,并且手動觸發KVO,隊列會監聽任務的執行狀態。由于我們設置了任務依賴,當任務6請求完成后才會執行任務5,任務5請求完成后 才會執行任務4。最后對各自任務進行移除隊列并釋放。其實這樣也變相解決了上面重寫main方法中無法解決的問題。

二.實際應用

執行

多個請求完成后繼續進行下一個請求的方法總結

在我們的工作中經常會遇到這樣的請求:一個請求依賴另一個請求的結果,或者多個請求一起發出然后再獲取所有的結果后繼續后續操作。根據這幾種情況總結常用的方法:

1. 使用GCDdispatch_group_t實現

需求:請求順序執行,執行完成后回調結果

 NSString *str = @"https://www.360.cn";
 NSURL *url = [NSURL URLWithString:str];
 NSURLRequest *request = [NSURLRequest requestWithURL:url];
 NSURLSession *session = [NSURLSession sharedSession];
   
 dispatch_group_t downloadGroup = dispatch_group_create();
 for (int i=0; i<10; i++) {
       dispatch_group_enter(downloadGroup);
       NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
           
          NSLog(@"執行完請求=%d",i);
          dispatch_group_leave(downloadGroup);
       }];
       
       [task resume];
   }
   dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
       NSLog(@"end");
   });
/*
2021-06-22 18:37:56.786878+0800 Interview01-打印[17121:9352056] 請求結束:0
2021-06-22 18:37:56.787770+0800 Interview01-打印[17121:9352057] 請求結束:1
2021-06-22 18:37:56.788492+0800 Interview01-打印[17121:9352057] 請求結束:2
2021-06-22 18:37:56.789148+0800 Interview01-打印[17121:9352057] 請求結束:3
2021-06-22 18:37:56.789837+0800 Interview01-打印[17121:9352057] 請求結束:4
2021-06-22 18:37:56.790433+0800 Interview01-打印[17121:9352059] 請求結束:5
2021-06-22 18:37:56.791117+0800 Interview01-打印[17121:9352059] 請求結束:6
2021-06-22 18:37:56.791860+0800 Interview01-打印[17121:9352059] 請求結束:7
2021-06-22 18:37:56.792614+0800 Interview01-打印[17121:9352059] 請求結束:8
2021-06-22 18:37:56.793201+0800 Interview01-打印[17121:9352059] 請求結束:9
2021-06-22 18:37:56.804529+0800 Interview01-打印[17121:9351753] end*/

主要方法:

  • dispatch_group_t downloadGroup = dispatch_group_create();創建隊列組
  • dispatch_group_enter(downloadGroup); 每次執行請求前調用
  • dispatch_group_leave(downloadGroup); 請求完成后調用離開方法
  • dispatch_group_notify() 所有請求完成后回調block
  • 對于enter和leave必須配合使用,有幾次enter就要有幾次leave

2. GCD信號量dispatch_semaphore_t

(1).需求:順序執行多個請求,都執行完成后回調給end
NSString *str = @"https://www.360.cn";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
       
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
   
for (int i=0; i<10; i++) {
      
     NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
           
           NSLog(@"請求結束:%d",i);
           dispatch_semaphore_signal(sem);
     }];
     [task resume];
     dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}
   dispatch_async(dispatch_get_main_queue(), ^{
       NSLog(@"end");
   });

主要方法

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_semaphore_signal(sem);
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

dispatch_semaphore信號量為基于計數器的一種多線程同步機制,dispatch_semaphore_signal(sem);表示為計數+1操作,dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 信號量-1,遇到dispatch_semaphore_wait如果信號量的值小于0,就一直阻塞線程,不執行后面的所有程序,直到信號量大于等于0;當第一個for循環執行后dispatch_semaphore_wait堵塞線程,直到執行到dispatch_semaphore_signal后繼續下一個for循環進行請求,以此類推完成順序請求。

(2).需求:多個請求同時進行,都執行完成后回調給end
NSString *str = @"https://www.360.cn";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
       
 dispatch_semaphore_t sem = dispatch_semaphore_create(0);
 __block int count = 0;
 for (int i=0; i<10; i++) {
           
      NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
               
        NSLog(@"%d---%d",i,i);
        count++;
        if (count==10) {
            dispatch_semaphore_signal(sem);
            count = 0;
         }
       }];
           
       [task resume];
   }
   dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
       
   dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"end");
   });
/*
2021-06-23 09:47:49.723576+0800 Interview01-打印[21740:9823752] 請求完成:0
2021-06-23 09:47:49.741118+0800 Interview01-打印[21740:9823751] 請求完成:1
2021-06-23 09:47:49.756781+0800 Interview01-打印[21740:9823752] 請求完成:3
2021-06-23 09:47:49.765250+0800 Interview01-打印[21740:9823752] 請求完成:2
2021-06-23 09:47:49.773008+0800 Interview01-打印[21740:9823756] 請求完成:4
2021-06-23 09:47:49.797809+0800 Interview01-打印[21740:9823751] 請求完成:5
2021-06-23 09:47:49.801775+0800 Interview01-打印[21740:9823751] 請求完成:6
2021-06-23 09:47:49.805542+0800 Interview01-打印[21740:9823751] 請求完成:7
2021-06-23 09:47:49.814714+0800 Interview01-打印[21740:9823751] 請求完成:8
2021-06-23 09:47:49.850517+0800 Interview01-打印[21740:9823753] 請求完成:9
2021-06-23 09:47:49.864394+0800 Interview01-打印[21740:9823591] end
*/

這個也比較好理解,for循環運行后堵塞當前線程(當前是主線程,你也可以把這段代碼放入子線程中去執行),當10個請求全部完成后發送信號,繼續下面的流程。

3. 使用NSOperationGCD結合使用

需求:兩個網絡請求,第一個依賴第二個的回調結果

通過自定義operation實現,我們重寫其main方法

@interface CustomOperation : NSOperation
@property (nonatomic, copy) id obj;
- (instancetype)initWithObject:(id)obj;
@end
@implementation CustomOperation

- (instancetype)initWithObject:(id)obj{
    if(self = [super init]){
        self.obj = obj;
    }
    return  self;
}

- (void)main{
    
    //創建信號量并設置計數默認為0
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    NSLog(@"開始執行任務%@",self.obj);
    NSString *str = @"https://www.360.cn";
    NSURL *url = [NSURL URLWithString:str];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    NSURLSession *session = [NSURLSession sharedSession];
    
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"TASK完成:====%@ thread====%@",self.obj,[NSThread currentThread]);
        //請求成功 計數+1操作
        dispatch_semaphore_signal(sema);
    }];

    [task resume];
    
    //若計數為0則一直等待
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    
}

調用與結果

 NSOperationQueue *queue3 = [[NSOperationQueue alloc] init];
    [queue3 setMaxConcurrentOperationCount:2];
    CustomOperation *operation0 = [[CustomOperation alloc] initWithObject:@"我是任務0"];
    CustomOperation *operation1 = [[CustomOperation alloc] initWithObject:@"我是任務1"];
    CustomOperation *operation2 = [[CustomOperation alloc] initWithObject:@"我是任務2"];
    CustomOperation *operation3 = [[CustomOperation alloc] initWithObject:@"我是任務3"];

    [operation0 addDependency:operation1];
    [operation1 addDependency:operation2];
    [operation2 addDependency:operation3];

    [queue3 addOperation:operation0];
    [queue3 addOperation:operation1];
    [queue3 addOperation:operation2];
    [queue3 addOperation:operation3];
/**打印結果
開始執行任務我是任務3
TASK完成:====我是任務3 thread====<NSThread: 0x6000039c3340>{number = 5, name = (null)}
開始執行任務我是任務2
TASK完成:====我是任務2 thread====<NSThread: 0x6000039ece80>{number = 7, name = (null)}
開始執行任務我是任務1
TASK完成:====我是任務1 thread====<NSThread: 0x6000039c3340>{number = 5, name = (null)}
開始執行任務我是任務0
TASK完成:====我是任務0 thread====<NSThread: 0x6000039c3d00>{number = 6, name = (null)}
*/
  • 設置任務依賴并且添加到隊列后是可以滿足我們的需求
  • 由于任務內部是異步回調,可以看到任務內部的執行還是依賴于dispatch_semaphore_t來實現的
  • 也可以通過重寫start方法實現,在上面章節我們已經介紹過了,這里不再贅述。

三. 總結

本文的篇幅有點長了,但是還有一些內容沒有覆蓋到,比如iOS中常用的線程鎖、NSOperationQueue的暫停與取消等,我們會在后面的文章中逐步完善補充。

由于作者水平有限,難免出現紕漏,如有問題還請不吝賜教。

參考資料:

蘋果官方——并發編程指南:Operation Queues

iOS GCD之dispatch_semaphore(信號量)

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容