iOS多線程使用總結(jié)

image

一.概述與實(shí)現(xiàn)方案

1. 線程與進(jìn)程

多線程在iOS中有著舉足輕重的地位阱冶,是每一位開發(fā)者都必備的技能舔示,當(dāng)然也是面試潮胫茫考的技術(shù)點(diǎn)撑螺,本文主要是探究我們實(shí)際開發(fā)或者面試中遇到的多線程問題。比如什么是線程城瞎?它跟進(jìn)程是什么關(guān)系渤闷,隊(duì)列跟線程什么關(guān)系,同步脖镀、異步飒箭、并發(fā)(并行)、串行這些概念又怎么來理解,iOS有哪些常用多線程方案补憾,以及線程同步技術(shù)有哪些等等漫萄。

線程(英語:thread)是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。大部分情況下盈匾,它被包含在進(jìn)程之中,是進(jìn)程中的實(shí)際運(yùn)作單位毕骡。一條線程指的是進(jìn)程中一個(gè)單一順序的控制流削饵,一個(gè)進(jìn)程中可以并發(fā)多個(gè)線程,每條線程并行執(zhí)行不同的任務(wù)未巫。 --- 維基百科

這里又多了一個(gè) 進(jìn)程,那什么是進(jìn)程呢,說白了就是是指在操作系統(tǒng)中正在運(yùn)行的一個(gè)應(yīng)用程序窿撬,如微信、支付寶app等都是一個(gè)進(jìn)程叙凡。線程是就是進(jìn)程的基本執(zhí)行單元劈伴,一個(gè)進(jìn)程的所有任務(wù)都在線程中執(zhí)行。也就是說 一個(gè)進(jìn)程最少要有一個(gè)線程握爷,這個(gè)線程就是主線程跛璧。當(dāng)然在我們實(shí)際使用過程中不可能只有一條主線程,我們?yōu)樘岣叱绦虻膱?zhí)行效率新啼,往往需要開辟多條子線程去執(zhí)行一些耗時(shí)任務(wù)追城,這里就引出了多線程的概念。

多線程(英語:multithreading)燥撞,是指從軟件或者硬件上實(shí)現(xiàn)多個(gè)線程并發(fā)執(zhí)行的技術(shù)

根據(jù)操作系統(tǒng)與硬件的不同分為兩類:軟件多線程硬件多線程

  • 軟件多線程: 即便CPU只能運(yùn)行一個(gè)線程座柱,操作系統(tǒng)也可以通過快速的在不同線程之間進(jìn)行切換,由于時(shí)間間隔很小物舒,來給用戶造成一種多個(gè)線程同時(shí)運(yùn)行的假象

  • 硬件多線程: 如果CPU有多個(gè)核心色洞,操作系統(tǒng)可以讓每個(gè)核心執(zhí)行一條線程,從而具有真正的同時(shí)執(zhí)行多個(gè)線程的能力冠胯,當(dāng)然由于任務(wù)數(shù)量遠(yuǎn)遠(yuǎn)多于CPU的核心數(shù)量火诸,所以,操作系統(tǒng)也會(huì)自動(dòng)把很多任務(wù)輪流調(diào)度到每個(gè)核心上執(zhí)行涵叮。
    以上都是google出來的一大堆東西惭蹂,比較抽象,沒關(guān)系我們來看下我們實(shí)際iOS開發(fā)中用到的多線程技術(shù)割粮。

2.iOS中的多線程方案

iOS 中的多線程方案主要有四種 PThread盾碗、NSThreadGCD舀瓢、NSOperation廷雅,PThread 是一套純粹C語言的API,能適用于Unix\Linux\Windows等系統(tǒng),線程生命周期需要程序員自己管理,使用難度較大航缀,在我們的實(shí)際開發(fā)中幾乎用不到商架,在這里我們不做過多介紹,感興趣的直接去百度芥玉。我們著重介紹另外三種方案蛇摸。

這里解釋一下線程的生命周期,所謂的線程的生命周期就是線程從創(chuàng)建到死亡的過程灿巧。一般會(huì)經(jīng)歷:新建 - 就緒 - 運(yùn)行 - 阻塞 - 死亡的過程赶袄。

  • 新建:就是初始化線程對(duì)象
  • 就緒:向線程對(duì)象發(fā)送start消息,線程對(duì)象被加入可調(diào)度線程池等待CPU調(diào)度抠藕。
  • 運(yùn)行:CPU 負(fù)責(zé)調(diào)度可調(diào)度線程池中線程的執(zhí)行饿肺,線程執(zhí)行完成之前,狀態(tài)可能會(huì)在就緒和運(yùn)行之間來回切換盾似。就緒和運(yùn)行之間的狀態(tài)變化由CPU負(fù)責(zé)敬辣,程序員不能干預(yù)。
  • 阻塞:當(dāng)滿足某個(gè)預(yù)定條件時(shí)零院,可以使用休眠或鎖溉跃,阻塞線程執(zhí)行
  • 死亡:線程執(zhí)行完畢,退出门粪,銷毀喊积。

(1) NSThread

NSThread是蘋果官方提供面向?qū)ο蟛僮骶€程的技術(shù),簡(jiǎn)單方便玄妈,可以直接操作線程對(duì)象乾吻,不過需要自己控制線程的生命周期,我們看下蘋果官方給出的方法拟蜻。

[1] 初始化方法
  • 實(shí)例初始化方法
- (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));

對(duì)應(yīng)的初始化方法:

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

注意三種方法創(chuàng)建完成后都需要執(zhí)行 [newThread start] 去啟動(dòng)線程绎签。

  • 類初始化方法
+ (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;

注意這兩個(gè)類方法創(chuàng)建后就可執(zhí)行,不需手動(dòng)開啟

[2] 取消退出

既然有了創(chuàng)建酝锅,那就得有退出

// 實(shí)例方法 取消線程
- (void)cancel;
//類方法 退出
+ (void)exit;
[3] 線程執(zhí)行狀態(tài)
// 線程正在執(zhí)行
@property (readonly, getter=isExecuting) BOOL executing API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
// 線程執(zhí)行結(jié)束
@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)
/*
* 去主線程執(zhí)行指定方法
* aSelector: 方法
* arg: 參數(shù)
* wait:表示是否等待主線程做完事情后往下走诡必,YES表示做完后執(zhí)行下面事情,NO表示跟下面事情一起執(zhí)行
*/
- (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;
/*
* 去指定線程執(zhí)行指定方法
* aSelector: 方法
* arg: 參數(shù)
* wait:表示是否等待本線程做完事情后往下走搔扁,YES表示做完后執(zhí)行下面事爸舒,NO表示跟下面事一起執(zhí)行
*/
- (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));
/*
* 去開啟的子線程執(zhí)行指定方法
* SEL: 方法
* arg: 參數(shù)
*/
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

我們常說的線程間的通信所用的方法其實(shí)就是上面的這幾個(gè)方法,所有繼承NSObject實(shí)例化對(duì)象都可調(diào)用稿蹲。當(dāng)然還有其他方法也可以實(shí)現(xiàn)線程間的通信扭勉,如:GCDNSOperation苛聘、NSMachPort端口等形式涂炎,我們后面用到在做介紹忠聚。
舉個(gè)簡(jiǎn)單的例子:我們?cè)谧泳€程中下載圖片,然后去主線程展示:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 子線程執(zhí)行下載方法
    [self performSelectorInBackground:@selector(download) withObject:nil];
}
- (void)download{
    //圖片的網(wǎng)絡(luò)路徑
     NSURL *url = [NSURL URLWithString:@"https://p3.ssl.qhimg.com/t011e94f0b9ed8e66b0.png"];
     //下載圖片數(shù)據(jù)
     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 獲取當(dāng)前線程
  • +(BOOL)isMultiThreaded 判斷當(dāng)前是否運(yùn)行在子線程
  • -(BOOL)isMainThread 判斷是否在主線程
  • +(void)sleepUntilDate:(NSDate *)date;+ (void)sleepForTimeInterval:(NSTimeInterval)ti; 當(dāng)前線程休眠時(shí)間

(2) GCD

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

[1]. 同步唱捣、異步两蟀、并發(fā)(并行)、串行
  • 同步和異步主要影響:能不能開啟新的線程
    同步:在當(dāng)前線程中執(zhí)行任務(wù)震缭,不具備開啟新線程的能力
    異步:在新的線程中執(zhí)行任務(wù)赂毯,具備開啟新線程的能力

  • 并發(fā)和串行主要影響:任務(wù)的執(zhí)行方式
    并發(fā):也叫并行,也叫并行隊(duì)列蛀序,多個(gè)任務(wù)并發(fā)(同時(shí))執(zhí)行
    串行:也叫串行隊(duì)列欢瞪,一個(gè)任務(wù)執(zhí)行完畢后,再執(zhí)行下一個(gè)任務(wù)

單純的介紹概念比較抽象徐裸,我們還是結(jié)合實(shí)際使用來說明:

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

可以看到這個(gè)兩個(gè)方法需要兩個(gè)參數(shù)啸盏,第一個(gè)參數(shù)需要傳入一個(gè)dispatch_queue_t 類型的隊(duì)列重贺,第二個(gè)是執(zhí)行的block。下面介紹一下GCD的隊(duì)列

[3] GCD 中的隊(duì)列

GCD中的隊(duì)列有三種:串行隊(duì)列回懦、并行隊(duì)列气笙、主隊(duì)列,創(chuàng)建方式也非常簡(jiǎn)單:

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

第一個(gè)參數(shù)是隊(duì)列名稱怯晕,第二個(gè)是一個(gè)宏定義潜圃,常用的兩個(gè)宏 DISPATCH_QUEUE_SERIALDISPATCH_QUEUE_CONCURRENT分別表示串行隊(duì)列和并行隊(duì)列,除此之外,宏DISPATCH_QUEUE_SERIAL_INACTIVEDISPATCH_QUEUE_CONCURRENT_INACTIVE 分別表示初始化的串行隊(duì)列和并行隊(duì)列處于不可活動(dòng)狀態(tài)舟茶√菲冢看下它的底層實(shí)現(xiàn)

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)

應(yīng)當(dāng)注意的是,初始化后處于不可活動(dòng)狀態(tài)的隊(duì)列吧凉,添加到其中的任務(wù)要想開始執(zhí)行隧出,必須先調(diào)用 dispatch_activate()函數(shù)使其狀態(tài)變更為可活動(dòng)狀態(tài).

  • 并行隊(duì)列
    并行隊(duì)列有兩種:

    第一種:全局并發(fā)隊(duì)列創(chuàng)建方法,也是系統(tǒng)為我們創(chuàng)建好的并發(fā)隊(duì)列,創(chuàng)建方式
/*  - 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);

這里有兩個(gè)參數(shù),第一個(gè)參數(shù)標(biāo)識(shí)線程執(zhí)行優(yōu)先級(jí)阀捅,第二個(gè)是蘋果保留參數(shù)傳參:0 就可以胀瞪。

第二種:手動(dòng)創(chuàng)建并發(fā)隊(duì)列

// 串行執(zhí)行,第一個(gè)參數(shù)是名稱 饲鄙,第二個(gè)是標(biāo)識(shí):DISPATCH_QUEUE_CONCURRENT,并發(fā)隊(duì)列標(biāo)識(shí)
dispatch_queue_t queue = dispatch_queue_create("myQueue",DISPATCH_QUEUE_CONCURRENT);
  • 主隊(duì)列
    主隊(duì)列是一種特殊的串行隊(duì)列
dispatch_queue_t queue = dispatch_get_main_queue();

同步凄诞、異步以及隊(duì)列的組合就可以實(shí)現(xiàn)對(duì)任務(wù)進(jìn)行多線程編程的需求了。

  1. 同步串行隊(duì)列
  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

可以看到?jīng)]有開啟新的線程忍级,都是在主線程中執(zhí)行任務(wù),并且是順序執(zhí)行的

  1. 同步并行隊(duì)列
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

也是在主線程中順序執(zhí)行帆谍。

  1. 異步串行隊(duì)列
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);
        });
    }

開啟子線程,順序執(zhí)行任務(wù)

  1. 異步并發(fā)隊(duì)列
 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
*/

開啟了多個(gè)子線程颤练,并且是并發(fā)執(zhí)行任務(wù)既忆。

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

  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

主隊(duì)列是一種特殊的串行隊(duì)列跃脊,從打印結(jié)果看出,這里執(zhí)行方式是串行苛吱,而且沒有開啟新的線程酪术。

具體任務(wù)的執(zhí)行方式可以參考下面的表格


執(zhí)行方式
[4] dispatch_ group_ t 隊(duì)列組

dispatch_group_t是一個(gè)比較實(shí)用的方法,通過構(gòu)造一個(gè)組的形式翠储,將各個(gè)同步或異步提交任務(wù)都加入到同一個(gè)組中绘雁,當(dāng)所有任務(wù)都完成后會(huì)收到通知,用于進(jìn)一步處理.舉個(gè)簡(jiǎn)單的例子如下:

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 柵欄異步調(diào)用函數(shù)

有異步調(diào)用就也有同步調(diào)用函數(shù)diapatch_barrier_sync()援所,兩者的區(qū)別:dispatch_barrier_sync 需要等待柵欄執(zhí)行完才會(huì)執(zhí)行柵欄后面的任務(wù),而dispatch_barrier_async 無需等待柵欄執(zhí)行完,會(huì)繼續(xù)往下走庐舟,有什么用呢?其實(shí)柵欄函數(shù)用的最多的地方還是實(shí)現(xiàn)線程同步使用住拭,比如我們有這樣一個(gè)需求:怎么樣利用GCD實(shí)現(xiàn)多讀單寫文件的IO操作挪略?也就是怎么樣實(shí)現(xiàn)多讀單寫,看代碼:

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

// 多讀單寫模型
@implementation UserCenter

- (id)init
{
    self = [super init];
    if (self) {
        // 通過宏定義 DISPATCH_QUEUE_CONCURRENT 創(chuàng)建一個(gè)并發(fā)隊(duì)列
        concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        // 創(chuàng)建數(shù)據(jù)容器
        userCenterDic = [NSMutableDictionary dictionary];
    }
    
    return self;
}

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

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

可以看到把寫操作放入柵欄函數(shù)滔岳,可以實(shí)現(xiàn)線程同步效果
注意:使用dispatch_barrier_async 杠娱,該函數(shù)只能搭配自定義并發(fā)隊(duì)列 dispatch_queue_t 使用。不能使用全局并發(fā)隊(duì)列: dispatch_get_global_queue谱煤,否則 dispatch_barrier_async無作用摊求。

[6] 線程死鎖

先來看兩個(gè)例子:

dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"執(zhí)行任務(wù)2");
    });// 往主線程里面 同步添加任務(wù) 會(huì)發(fā)生死鎖現(xiàn)象

 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

上面的例子可以看出,不能向當(dāng)前的串行隊(duì)列刘离,同步添加任務(wù)室叉,否則會(huì)產(chǎn)生死鎖導(dǎo)致crash。線程死鎖的條件:使用sync函數(shù)往當(dāng)前串行隊(duì)列里面添加任務(wù)寥闪,會(huì)產(chǎn)生死鎖太惠。

(3) NSOperation

NSOperation 是蘋果對(duì)GCD面向?qū)ο蟮姆庋b,它的底層是基于GCD實(shí)現(xiàn)的疲憋,相比于GCD它添加了更多實(shí)用的功能

  • 可以添加任務(wù)依賴
  • 任務(wù)執(zhí)行狀態(tài)的控制
  • 設(shè)置最大并發(fā)數(shù)
    它有兩個(gè)核心類分別是NSOperationNSOperationQueue凿渊,NSOperation就是對(duì)任務(wù)進(jìn)行的封裝,封裝好的任務(wù)交給不同的NSOperationQueue即可進(jìn)行串行隊(duì)列的執(zhí)行或并發(fā)隊(duì)列的執(zhí)行缚柳。
[1] NSOperation

NSOperation 是一個(gè)抽象類埃脏,并不能直接使用,必須使用它的子類秋忙,有三種方式:NSInvocationOperation彩掐、NSBlockOperation自定義子類繼承NSOperation,前兩種是蘋果為我們封裝好的灰追,可以直接使用堵幽,自定義子類狗超,需要我們實(shí)現(xiàn)相應(yīng)的方法。

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

/*
創(chuàng)建一個(gè)NSInvocationOperation對(duì)象努咐,指定執(zhí)行的對(duì)象和方法
該方法可以接收一個(gè)參數(shù)即object
*/
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task:) object:@"Hello, World!"];

// 執(zhí)行
[operation start];
[invocationOperation start];

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

可以看到創(chuàng)建這兩個(gè)任務(wù)對(duì)象去執(zhí)行任務(wù)殴胧,并沒有開啟新線程渗稍。NSBlockOperation 相比 NSInvocationOperation 多了個(gè)addExecutionBlock 追加任務(wù)的方法,

 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追加的任務(wù)是并發(fā)執(zhí)行的,如果這個(gè)操作的任務(wù)數(shù)大于1那么會(huì)開啟子線程并發(fā)執(zhí)行任務(wù)团滥,這里追加的任務(wù)不一定就是子線程竿屹,也有可能是主線程。

[2] NSOperationQueue

NSOperationQueue 有兩種隊(duì)列灸姊,一個(gè)是主隊(duì)列通過[NSOperationQueue mainQueue] 獲取拱燃,還有一個(gè)是自己創(chuàng)建的隊(duì)列[[NSOperationQueue alloc] init],它同時(shí)具備并發(fā)跟串行的能力,可以通過設(shè)置最大并發(fā)數(shù)來決定力惯。

 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];//設(shè)置最大并發(fā)數(shù)扼雏,如果設(shè)置為1則串行執(zhí)行
    [queues addOperation:operation];
    [queues addOperation:operation2];
/*
Task2===== <NSThread: 0x600000489940>{number = 4, name = (null)} 0
task1=====<NSThread: 0x6000004e15c0>{number = 5, name = (null)} 0
*/

這個(gè)例子有兩個(gè)任務(wù),如果設(shè)置最大并發(fā)數(shù)為2夯膀,則會(huì)開辟兩個(gè)線程,并發(fā)執(zhí)行這兩個(gè)任務(wù)苍蔬。如果設(shè)置為1诱建,則會(huì)在新的線程中串行執(zhí)行。

[3] 任務(wù)依賴

addDependency可以建立兩個(gè)任務(wù)之間的依賴關(guān)系碟绑,如[operation2 addDependency:operation1]; 為任務(wù)2依賴任務(wù)1俺猿,必須等任務(wù)1執(zhí)行完成后才會(huì)執(zhí)行任務(wù)2,看個(gè)例子

  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];
    //設(shè)置任務(wù)依賴
    [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
*/

還是以上面的例子格仲,設(shè)置[operation addDependency:operation2];,可以看到任務(wù)2完成后才會(huì)執(zhí)行任務(wù)1的操作押袍。

[4] 自定義NSOperation

任務(wù)執(zhí)行狀態(tài)的控制是相對(duì)于自定義的NSOperation子類來說的。對(duì)于自定義NSOperation子類有兩種類型:

  1. 重寫main方法
    只重寫operation的main方法,main方法里面寫要執(zhí)行的任務(wù)凯肋,系統(tǒng)底層控制變更任務(wù)執(zhí)行完成狀態(tài)谊惭,以及任務(wù)的退出∥甓看個(gè)例子
#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(@"開始執(zhí)行任務(wù)%@ thread===%@",self.obj,[NSThread currentThread]);
}

調(diào)用

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

可以看到任務(wù)operation的main方法執(zhí)行是在主線程中的圈盔,只是最后完成后的回調(diào)setCompletionBlock是異步的,好像沒什么用悄雅,別著急驱敲,我們把他放入隊(duì)列中執(zhí)行看下,還是上面的例子,加入隊(duì)列執(zhí)行

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

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

這時(shí)候可以看到任務(wù)的并發(fā)執(zhí)行了众眨,operation的main方法執(zhí)行結(jié)束后就會(huì)調(diào)用各自的dealloc方法進(jìn)行釋放握牧,任務(wù)的生命周期結(jié)束。如果我們想讓任務(wù)4娩梨、5庄蹋、6 倒序執(zhí)行,可以添加任務(wù)依賴

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

這樣做貌似是可以的双藕,但是如果我們的operation 中又存在異步任務(wù)(如網(wǎng)絡(luò)請(qǐng)求)币励,我們想讓網(wǎng)絡(luò)任務(wù)6請(qǐng)求完后調(diào)用任務(wù)5,任務(wù)5調(diào)用成功后調(diào)任務(wù)4掸冤,那該怎么辦呢厘托,我們先賣個(gè)關(guān)子,我們?cè)诘诙?jié)多個(gè)請(qǐng)求完成后繼續(xù)進(jìn)行下一個(gè)請(qǐng)求的方法總結(jié)中介紹稿湿。

  1. 重寫start方法
    通過重寫main方法可以實(shí)現(xiàn)任務(wù)的串行執(zhí)行铅匹,如果要讓任務(wù)并發(fā)執(zhí)行,就需要重寫start方法饺藤。兩者還是有很大區(qū)別的:
    如果只是重寫main方法包斑,方法執(zhí)行完畢,那么整個(gè)operation就會(huì)從隊(duì)列中被移除涕俗。如果你是一個(gè)自定義的operation并且它是某些類的代理罗丰,這些類恰好有異步方法,這時(shí)就會(huì)找不到代理導(dǎo)致程序出錯(cuò)了再姑。然而start方法就算執(zhí)行完畢萌抵,它的finish屬性也不會(huì)變,因此你可以控制這個(gè)operation的生命周期了元镀。然后在任務(wù)完成之后手動(dòng)cancel掉這個(gè)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{
    
    //在任務(wù)開始前設(shè)置executing為YES,在此之前可能會(huì)進(jìn)行一些初始化操作
    self.executing = YES;
    NSLog(@"開始執(zhí)行任務(wù)%@ thread===%@",self.obj,[NSThread currentThread]);
    /*
    需要在適當(dāng)?shù)奈恢门袛嗤獠渴欠裾{(diào)用了cancel方法
    如果被cancel了需要正確的結(jié)束任務(wù)
    */
    if (self.isCancelled)
    {
        //任務(wù)被取消正確結(jié)束前手動(dòng)設(shè)置狀態(tài)
        self.executing = NO;
        self.finished = YES;
        return;
    }
    
    NSString *str = @"https://www.#";
    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]);
        //任務(wù)執(zhí)行完成后手動(dòng)設(shè)置狀態(tài)
        weakSelf.executing = NO;
        weakSelf.finished = YES;
    }];
    [task resume];
}
- (void)setExecuting:(BOOL)executing
{
    //手動(dòng)調(diào)用KVO通知
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    //調(diào)用KVO通知
    [self didChangeValueForKey:@"isExecuting"];
}
- (BOOL)isExecuting
{
    return _executing;
}
- (void)setFinished:(BOOL)finished
{
    //手動(dòng)調(diào)用KVO通知
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    //調(diào)用KVO通知
    [self didChangeValueForKey:@"isFinished"];
}
- (BOOL)isFinished
{
    return _finished;
}
- (BOOL)isAsynchronous
{
    return YES;
}
- (void)dealloc{
    NSLog(@"Dealloc %@",self.obj);
}

執(zhí)行與結(jié)果

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

在這個(gè)例子中我們?cè)谌蝿?wù)請(qǐng)求完成后栖疑,手動(dòng)設(shè)置其self.executingself.finished狀態(tài)讨永,并且手動(dòng)觸發(fā)KVO,隊(duì)列會(huì)監(jiān)聽任務(wù)的執(zhí)行狀態(tài)遇革。由于我們?cè)O(shè)置了任務(wù)依賴卿闹,當(dāng)任務(wù)6請(qǐng)求完成后才會(huì)執(zhí)行任務(wù)5,任務(wù)5請(qǐng)求完成后 才會(huì)執(zhí)行任務(wù)4澳淑。最后對(duì)各自任務(wù)進(jìn)行移除隊(duì)列并釋放比原。其實(shí)這樣也變相解決了上面重寫main方法中無法解決的問題。

二.實(shí)際應(yīng)用

執(zhí)行

多個(gè)請(qǐng)求完成后繼續(xù)進(jìn)行下一個(gè)請(qǐng)求的方法總結(jié)

在我們的工作中經(jīng)常會(huì)遇到這樣的請(qǐng)求:一個(gè)請(qǐng)求依賴另一個(gè)請(qǐng)求的結(jié)果杠巡,或者多個(gè)請(qǐng)求一起發(fā)出然后再獲取所有的結(jié)果后繼續(xù)后續(xù)操作量窘。根據(jù)這幾種情況總結(jié)常用的方法:

1. 使用GCDdispatch_group_t實(shí)現(xiàn)

需求:請(qǐng)求順序執(zhí)行,執(zhí)行完成后回調(diào)結(jié)果

 NSString *str = @"https://www.#";
 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(@"執(zhí)行完請(qǐng)求=%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] 請(qǐng)求結(jié)束:0
2021-06-22 18:37:56.787770+0800 Interview01-打印[17121:9352057] 請(qǐng)求結(jié)束:1
2021-06-22 18:37:56.788492+0800 Interview01-打印[17121:9352057] 請(qǐng)求結(jié)束:2
2021-06-22 18:37:56.789148+0800 Interview01-打印[17121:9352057] 請(qǐng)求結(jié)束:3
2021-06-22 18:37:56.789837+0800 Interview01-打印[17121:9352057] 請(qǐng)求結(jié)束:4
2021-06-22 18:37:56.790433+0800 Interview01-打印[17121:9352059] 請(qǐng)求結(jié)束:5
2021-06-22 18:37:56.791117+0800 Interview01-打印[17121:9352059] 請(qǐng)求結(jié)束:6
2021-06-22 18:37:56.791860+0800 Interview01-打印[17121:9352059] 請(qǐng)求結(jié)束:7
2021-06-22 18:37:56.792614+0800 Interview01-打印[17121:9352059] 請(qǐng)求結(jié)束:8
2021-06-22 18:37:56.793201+0800 Interview01-打印[17121:9352059] 請(qǐng)求結(jié)束:9
2021-06-22 18:37:56.804529+0800 Interview01-打印[17121:9351753] end*/

主要方法:

  • dispatch_group_t downloadGroup = dispatch_group_create();創(chuàng)建隊(duì)列組
  • dispatch_group_enter(downloadGroup); 每次執(zhí)行請(qǐng)求前調(diào)用
  • dispatch_group_leave(downloadGroup); 請(qǐng)求完成后調(diào)用離開方法
  • dispatch_group_notify() 所有請(qǐng)求完成后回調(diào)block
  • 對(duì)于enter和leave必須配合使用氢拥,有幾次enter就要有幾次leave

2. GCD信號(hào)量dispatch_semaphore_t

(1).需求:順序執(zhí)行多個(gè)請(qǐng)求蚌铜,都執(zhí)行完成后回調(diào)給end

NSString *str = @"https://www.#";
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(@"請(qǐng)求結(jié)束:%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信號(hào)量為基于計(jì)數(shù)器的一種多線程同步機(jī)制,dispatch_semaphore_signal(sem);表示為計(jì)數(shù)+1操作锨侯,dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 信號(hào)量-1,遇到dispatch_semaphore_wait如果信號(hào)量的值小于0,就一直阻塞線程,不執(zhí)行后面的所有程序,直到信號(hào)量大于等于0;當(dāng)?shù)谝粋€(gè)for循環(huán)執(zhí)行后dispatch_semaphore_wait堵塞線程冬殃,直到執(zhí)行到dispatch_semaphore_signal后繼續(xù)下一個(gè)for循環(huán)進(jìn)行請(qǐng)求囚痴,以此類推完成順序請(qǐng)求。

(2).需求:多個(gè)請(qǐng)求同時(shí)進(jìn)行审葬,都執(zhí)行完成后回調(diào)給end

NSString *str = @"https://www.#";
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] 請(qǐng)求完成:0
2021-06-23 09:47:49.741118+0800 Interview01-打印[21740:9823751] 請(qǐng)求完成:1
2021-06-23 09:47:49.756781+0800 Interview01-打印[21740:9823752] 請(qǐng)求完成:3
2021-06-23 09:47:49.765250+0800 Interview01-打印[21740:9823752] 請(qǐng)求完成:2
2021-06-23 09:47:49.773008+0800 Interview01-打印[21740:9823756] 請(qǐng)求完成:4
2021-06-23 09:47:49.797809+0800 Interview01-打印[21740:9823751] 請(qǐng)求完成:5
2021-06-23 09:47:49.801775+0800 Interview01-打印[21740:9823751] 請(qǐng)求完成:6
2021-06-23 09:47:49.805542+0800 Interview01-打印[21740:9823751] 請(qǐng)求完成:7
2021-06-23 09:47:49.814714+0800 Interview01-打印[21740:9823751] 請(qǐng)求完成:8
2021-06-23 09:47:49.850517+0800 Interview01-打印[21740:9823753] 請(qǐng)求完成:9
2021-06-23 09:47:49.864394+0800 Interview01-打印[21740:9823591] end
*/

這個(gè)也比較好理解深滚,for循環(huán)運(yùn)行后堵塞當(dāng)前線程(當(dāng)前是主線程,你也可以把這段代碼放入子線程中去執(zhí)行)涣觉,當(dāng)10個(gè)請(qǐng)求全部完成后發(fā)送信號(hào)痴荐,繼續(xù)下面的流程。

3. 使用NSOperationGCD結(jié)合使用

需求:兩個(gè)網(wǎng)絡(luò)請(qǐng)求官册,第一個(gè)依賴第二個(gè)的回調(diào)結(jié)果

通過自定義operation實(shí)現(xiàn)生兆,我們重寫其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{
    
    //創(chuàng)建信號(hào)量并設(shè)置計(jì)數(shù)默認(rèn)為0
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    NSLog(@"開始執(zhí)行任務(wù)%@",self.obj);
    NSString *str = @"https://www.#";
    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]);
        //請(qǐng)求成功 計(jì)數(shù)+1操作
        dispatch_semaphore_signal(sema);
    }];

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

調(diào)用與結(jié)果

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

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

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

三. 總結(jié)

本文的篇幅有點(diǎn)長了员淫,但是還有一些內(nèi)容沒有覆蓋到合蔽,比如iOS中常用的線程鎖、NSOperationQueue的暫停與取消等介返,我們會(huì)在后面的文章中逐步完善補(bǔ)充辈末。

由于作者水平有限,難免出現(xiàn)紕漏映皆,如有問題還請(qǐng)不吝賜教。

參考資料:

蘋果官方——并發(fā)編程指南:Operation Queues

iOS GCD之dispatch_semaphore(信號(hào)量)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末轰枝,一起剝皮案震驚了整個(gè)濱河市捅彻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鞍陨,老刑警劉巖步淹,帶你破解...
    沈念sama閱讀 216,919評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異诚撵,居然都是意外死亡缭裆,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門寿烟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澈驼,“玉大人,你說我怎么就攤上這事筛武》炱洌” “怎么了挎塌?”我有些...
    開封第一講書人閱讀 163,316評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長内边。 經(jīng)常有香客問我榴都,道長,這世上最難降的妖魔是什么漠其? 我笑而不...
    開封第一講書人閱讀 58,294評(píng)論 1 292
  • 正文 為了忘掉前任嘴高,我火速辦了婚禮,結(jié)果婚禮上和屎,老公的妹妹穿的比我還像新娘拴驮。我一直安慰自己,他們只是感情好眶俩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,318評(píng)論 6 390
  • 文/花漫 我一把揭開白布莹汤。 她就那樣靜靜地躺著,像睡著了一般颠印。 火紅的嫁衣襯著肌膚如雪纲岭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,245評(píng)論 1 299
  • 那天线罕,我揣著相機(jī)與錄音止潮,去河邊找鬼。 笑死钞楼,一個(gè)胖子當(dāng)著我的面吹牛喇闸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播询件,決...
    沈念sama閱讀 40,120評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼燃乍,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了宛琅?” 一聲冷哼從身側(cè)響起刻蟹,我...
    開封第一講書人閱讀 38,964評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎嘿辟,沒想到半個(gè)月后舆瘪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,376評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡红伦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,592評(píng)論 2 333
  • 正文 我和宋清朗相戀三年英古,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昙读。...
    茶點(diǎn)故事閱讀 39,764評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡召调,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情某残,我是刑警寧澤国撵,帶...
    沈念sama閱讀 35,460評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站玻墅,受9級(jí)特大地震影響介牙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜澳厢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,070評(píng)論 3 327
  • 文/蒙蒙 一环础、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧剩拢,春花似錦线得、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至办素,卻和暖如春角雷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背性穿。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評(píng)論 1 269
  • 我被黑心中介騙來泰國打工勺三, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人需曾。 一個(gè)月前我還...
    沈念sama閱讀 47,819評(píng)論 2 370
  • 正文 我出身青樓吗坚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親呆万。 傳聞我的和親對(duì)象是個(gè)殘疾皇子商源,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,665評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容