iOS多线程使用总结

image

一.概述与实现方案

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; 当前线程休眠时间

(2) 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函数往当前串行队列里面添加任务,会产生死锁。

(3) 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阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容