
一.概述与实现方案
1. 线程与进程
多线程在iOS中有着举足轻重的地位,是每一位开发者都必备的技能,当然也是面试常考的技术点,本文主要是探究我们实际开发或者面试中遇到的多线程问题。比如什么是线程?它跟进程是什么关系,队列跟线程什么关系,同步、异步、并发(并行)、串行
这些概念又怎么来理解,iOS有哪些常用多线程方案,以及线程同步技术有哪些等等。
线程(英语:thread)是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 — 维基百科
这里又多了一个 进程
,那什么是进程呢,说白了就是是指在操作系统中正在运行的一个应用程序,如微信、支付宝app等都是一个进程。线程是就是进程的基本执行单元,一个进程的所有任务都在线程中执行。也就是说 一个进程最少要有一个线程,这个线程就是主线程。当然在我们实际使用过程中不可能只有一条主线程,我们为提高程序的执行效率,往往需要开辟多条子线程去执行一些耗时任务,这里就引出了多线程的概念。
多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术
根据操作系统与硬件的不同分为两类:软件多线程
与硬件多线程
软件多线程: 即便CPU只能运行一个线程,操作系统也可以通过快速的在不同线程之间进行切换,由于时间间隔很小,来给用户造成一种多个线程同时运行的假象
硬件多线程: 如果CPU有多个核心,操作系统可以让每个核心执行一条线程,从而具有真正的同时执行多个线程的能力,当然由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
以上都是google出来的一大堆东西,比较抽象,没关系我们来看下我们实际iOS开发中用到的多线程技术。
2.iOS中的多线程方案
iOS 中的多线程方案主要有四种 PThread
、NSThread
、GCD
、NSOperation
,PThread
是一套纯粹C
语言的API,能适用于Unix\Linux\Windows等系统,线程生命周期需要程序员自己管理,使用难度较大,在我们的实际开发中几乎用不到,在这里我们不做过多介绍,感兴趣的直接去百度。我们着重介绍另外三中方案。
这里解释一下线程的生命周期,所谓的线程的生命周期就是线程从创建到死亡的过程。一般会经历:新建 - 就绪 - 运行 - 阻塞 - 死亡
的过程。
- 新建:就是初始化线程对象
- 就绪:向线程对象发送start消息,线程对象被加入可调度线程池等待CPU调度。
- 运行:CPU 负责调度可调度线程池中线程的执行,线程执行完成之前,状态可能会在就绪和运行之间来回切换。就绪和运行之间的状态变化由CPU负责,程序员不能干预。
- 阻塞:当满足某个预定条件时,可以使用休眠或锁,阻塞线程执行
- 死亡:线程执行完毕,退出,销毁。
(1) NSThread
NSThread是苹果官方提供面向对象操作线程的技术,简单方便,可以直接操作线程对象,不过需要自己控制线程的生命周期,我们看下苹果官方给出的方法。
[1] 初始化方法
- 实例初始化方法
1 2 3 4
| - (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));
|
对应的初始化方法:
1 2 3 4 5 6
| //创建线程 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]
去启动线程。
- 类初始化方法
1 2
| + (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] 取消退出
既然有了创建,那就得有退出
1 2 3 4
| // 实例方法 取消线程 - (void)cancel; //类方法 退出 + (void)exit;
|
[3] 线程执行状态
1 2 3 4 5 6
| // 线程正在执行 @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] 线程间的通信方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @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实例化对象都可调用。当然还有其他方法也可以实现线程间的通信,如:GCD
、NSOperation
、NSMachPort
端口等形式,我们后面用到在做介绍。
举个简单的例子:我们在子线程中下载图片,然后去主线程展示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| - (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()
使用方法:1 2
| 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中的队列有三种:串行队列、并行队列、主队列
,创建方式也非常简单:
- 串行队列
1
| dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
|
第一个参数是队列名称,第二个是一个宏定义,常用的两个宏 DISPATCH_QUEUE_SERIAL
和 DISPATCH_QUEUE_CONCURRENT
分别表示串行队列和并行队列,除此之外,宏DISPATCH_QUEUE_SERIAL_INACTIVE
和 DISPATCH_QUEUE_CONCURRENT_INACTIVE
分别表示初始化的串行队列和并行队列处于不可活动状态。看下它的底层实现1 2 3 4 5 6 7 8 9
| 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()
函数使其状态变更为可活动状态.
- 并行队列
并行队列有两种:
第一种:全局并发队列创建方法,也是系统为我们创建好的并发队列,创建方式
1 2 3 4 5 6 7 8 9
|
dispatch_queue_t queue = dispatch_get_global_queue(0,0);
|
这里有两个参数,第一个参数标识线程执行优先级,第二个是苹果保留参数传参:0 就可以。
第二种:手动创建并发队列
1 2
| dispatch_queue_t queue = dispatch_queue_create("myQueue",DISPATCH_QUEUE_CONCURRENT);
|
- 主队列
主队列是一种特殊的串行队列1
| dispatch_queue_t queue = dispatch_get_main_queue();
|
同步、异步以及队列的组合就可以实现对任务进行多线程编程的需求了。
同步串行队列
1 2 3 4 5 6 7
| 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 2 3 4 5 6 7
| 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
|
也是在主线程中顺序执行。
3. 异步串行队列
1 2 3 4 5 6
| 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); }); }
|
开启子线程,顺序执行任务
4. 异步并发队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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 是主队列,就是在主线程中执行任务,没有开辟新线程。
1 2 3 4 5 6 7 8
| 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
是一个比较实用的方法,通过构造一个组的形式,将各个同步或异步提交任务都加入到同一个组中,当所有任务都完成后会收到通知,用于进一步处理.举个简单的例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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操作?也就是怎么样实现多读单写,看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @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] 线程死锁
先来看两个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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它添加了更多实用的功能
- 可以添加任务依赖
- 任务执行状态的控制
- 设置最大并发数
它有两个核心类分别是NSOperation
和NSOperationQueue
,NSOperation就是对任务进行的封装,封装好的任务交给不同的NSOperationQueue即可进行串行队列的执行或并发队列的执行。
[1] NSOperation
NSOperation 是一个抽象类,并不能直接实用,必须使用它的子类,有三种方式:NSInvocationOperation
、NSBlockOperation
、自定义子类继承NSOperation
,前两中是苹果为我们封装好的,可以直接使用,自定义子类,需要我们实现相应的方法。
- NSBlockOperation & NSInvocationOperation
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| //创建一个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
追加任务的方法,1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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]
,它同时具备并发跟串行的能力,可以通过设置最大并发数来决定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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,看个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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子类有两种类型:
- 重写
main
方法
只重写operation
的main方法,main方法里面写要执行的任务,系统底层控制变更任务执行完成状态,以及任务的退出。看个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #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]); }
|
调用
1 2 3 4 5 6 7 8
| 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
是异步的,好像没什么用,别着急,我们把他放入队列中执行看下,还是上面的例子,加入队列执行
1 2 3 4 5 6 7 8 9 10 11 12
| 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 倒序执行,可以添加任务依赖
1 2 3 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,那该怎么办呢,我们先卖个关子,我们在第二节多个请求完成后继续进行下一个请求的方法总结
中介绍。
2. 重写start
方法
通过重写main
方法可以实现任务的串行执行,如果要让任务并发执行,就需要重写start
方法。两者还是有很大区别的:
如果只是重写main方法,方法执行完毕,那么整个operation就会从队列中被移除。如果你是一个自定义的operation并且它是某些类的代理,这些类恰好有异步方法,这时就会找不到代理导致程序出错了。然而start方法就算执行完毕,它的finish属性也不会变,因此你可以控制这个operation的生命周期了。然后在任务完成之后手动cancel掉这个operation即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| @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); }
|
执行与结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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.executing
和self.finished
状态,并且手动触发KVO,队列会监听任务的执行状态。由于我们设置了任务依赖,当任务6请求完成后才会执行任务5,任务5请求完成后 才会执行任务4。最后对各自任务进行移除队列并释放。其实这样也变相解决了上面重写main
方法中无法解决的问题。
二.实际应用

多个请求完成后继续进行下一个请求的方法总结
在我们的工作中经常会遇到这样的请求:一个请求依赖另一个请求的结果,或者多个请求一起发出然后再获取所有的结果后继续后续操作。根据这几种情况总结常用的方法:
1. 使用GCD
的dispatch_group_t
实现
需求:请求顺序执行,执行完成后回调结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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"); });
|
主要方法
1 2 3
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| 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. 使用NSOperation
与GCD
结合使用
需求:两个网络请求,第一个依赖第二个的回调结果
通过自定义operation
实现,我们重写其main方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @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); }
|
调用与结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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(信号量)