多线程实践
有段时间没写博客了,不过这也不是一次两次了。
嗯,就不找理由也不检讨了,直奔主题吧。
在今天的博客中你将会看到:
老司机今天讲的不是多线程的基本用法,这个东西往上的博客其实蛮多的,而且也基本是多线程的基本用法。老司机今天主要的是介绍多个异步线程执行结束后进行回调的解决方案,如果说这么说不太清楚的话,最常见的场景就是多个网络请求都结束后触发列表刷新。
其实这个需求呢,还是挺常见的。主要呢,目前有两种解决思路,一种呢是GCD中的dispatch_group,一种是NSOperation。
这个方案呢,实现起来还比较简单,先放一下代码吧。
-(void)testGCDGroup {
dispatch_group_t g = dispatch_group_create();
dispatch_queue_t q = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"Will enter task1");
dispatch_group_enter(g);
dispatch_group_async(g, q, ^{
[self task1];
dispatch_group_leave(g);
});
NSLog(@"Will enter task2");
dispatch_group_enter(g);
dispatch_group_async(g, q, ^{
[self task2];
dispatch_group_leave(g);
});
NSLog(@"Come to notify");
dispatch_group_notify(g, q, ^{
NSLog(@"Enter notify");
[self taskComplete];
});
NSLog(@"Pass notify");
}
-(void)task1 {
NSLog(@"Enter sleep 10.");
[NSThread sleepForTimeInterval:10];
NSLog(@"Leave sleep 10.");
}
-(void)task2 {
NSLog(@"Enter sleep 5.");
[NSThread sleepForTimeInterval:5];
NSLog(@"Leave sleep 5.");
}
-(void)taskComplete {
NSLog(@"All task finished.");
}
控制台输出是这个样子的:
2018-03-26 14:28:02.317556+0800 test[3446:287435] Will enter task1.
2018-03-26 14:28:02.317714+0800 test[3446:287435] Will enter task2.
2018-03-26 14:28:02.317733+0800 test[3446:287484] Enter sleep 10.
2018-03-26 14:28:02.317847+0800 test[3446:287435] Come to notify.
2018-03-26 14:28:02.317865+0800 test[3446:287486] Enter sleep 5.
2018-03-26 14:28:02.318093+0800 test[3446:287435] Pass notify.
2018-03-26 14:28:07.318474+0800 test[3446:287486] Leave sleep 5.
2018-03-26 14:28:12.321389+0800 test[3446:287484] Leave sleep 10.
2018-03-26 14:28:12.321740+0800 test[3446:287484] Enter notify.
2018-03-26 14:28:12.321932+0800 test[3446:287484] All task finished.
他呢,基本流程就是当调用的dispatch_group_leave()与dispatch_group_enter()相等时,就会调用dispatch_group_notify()中的回调。不过这种实现方案呢,还是有一个需要注意的点就是dispatch_group_enter()与dispatch_group_leave()要成对使用,否则就会进入无限的等待状态。
第二个解决方案就是使用NSOperation。呐,我会放在第二节着重介绍一下的。
我们知道,NSOperation是苹果提供的一套面向对象的基于GCD封装的多线程解决方案。他在使用上更加符合面向对象的思想,更加方便的为任务添加依赖关系,同时提供了四个支持KVO监听的代表当前任务执行状态的属性cancelled、executing、finished、ready。NSOperation内部对这四个状态行为作了预处理,根据任务的不同状态这四个属性的值会自动改变。当NSOperation配合NSOperationQueue使用时,Queue会监听所有Operation的状态从而分配任务的启动时机。总之,NSOperation隐藏了很多内部细节,让我们开发者无需关心任务的各种状态。
首先,为了模仿系统行为,我们先观察下系统的NSOperation的cancelled、executing、finished、ready四个属性的状态变化情况。那我们去监听一下NSOperation的四个属性。代码如下:
TestBlockOperation * bp1 = [TestBlockOperation blockOperationWithBlock:^{
NSLog(@"enter bp1");
[NSThread sleepForTimeInterval:3];
NSLog(@"leave bp1");
}];
NSArray * keyPathes = @[@"isReady",@"isCancelled",@"isExecuting",@"isFinished"];
[self logOp:bp1 keyPathes:keyPathes];
[self addObserverForOp:bp1 keyPathes:keyPathes];
[bp1 start];
[bp1 cancel];
控制台输出:
2018-04-18 11:45:01.277354+0800 OperationDemo[72212:1655503] bp1 isReady = true
2018-04-18 11:45:01.277539+0800 OperationDemo[72212:1655503] bp1 isCancelled = false
2018-04-18 11:45:01.278212+0800 OperationDemo[72212:1655503] bp1 isExecuting = false
2018-04-18 11:45:01.278449+0800 OperationDemo[72212:1655503] bp1 isFinished = false
2018-04-18 11:45:01.278682+0800 OperationDemo[72212:1655503] bp1 before start
2018-04-18 11:45:01.278954+0800 OperationDemo[72212:1655503] bp1---isExecuting---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 11:45:01.279063+0800 OperationDemo[72212:1655503] bp1 before main
2018-04-18 11:45:01.279245+0800 OperationDemo[72212:1655503] enter bp1
2018-04-18 11:45:04.279669+0800 OperationDemo[72212:1655503] leave bp1
2018-04-18 11:45:04.280074+0800 OperationDemo[72212:1655503] bp1 after main
2018-04-18 11:45:04.281164+0800 OperationDemo[72212:1655503] bp1---isExecuting---{
kind = 1;
new = 0;
old = 1;
}
2018-04-18 11:45:04.281404+0800 OperationDemo[72212:1655503] bp1---isFinished---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 11:45:04.281557+0800 OperationDemo[72212:1655503] bp1 after start
2018-04-18 11:45:04.281782+0800 OperationDemo[72212:1655503] bp1 before cancel
2018-04-18 11:45:04.281917+0800 OperationDemo[72212:1655503] bp1 after cancel
上述代码中,我们监听了四个属性并执行了Operation。根据日志我们可以总结如下:
初始状态下,ready为YES,其他均为NO 当我们调用 -start 后,执行 -main 之前 isExecuting 属性从NO被置为YES 调用 -main 之后开始执行提交到Operation中的任务 任务完成后 isExecuting 属性从YES被置为NO,isFinished 属性从NO被置为YES
我们再看一下如果在执行 -start 之前先执行 -cancel 后会是什么状态:
TestBlockOperation * bp2 = [TestBlockOperation blockOperationWithBlock:^{
NSLog(@"enter bp2");
[NSThread sleepForTimeInterval:3];
NSLog(@"leave bp2");
}];
[self addObserverForObj:bp2 keyPathes:keyPathes];
self.bp2 = bp2;
[bp2 cancel];
[bp2 start];
控制台输出:
2018-04-18 11:44:03.597414+0800 OperationDemo[72184:1653790] bp2 before cancel
2018-04-18 11:44:03.597684+0800 OperationDemo[72184:1653790] bp2---isCancelled---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 11:44:03.597881+0800 OperationDemo[72184:1653790] bp2---isReady---{
kind = 1;
new = 1;
old = 1;
}
2018-04-18 11:44:03.598051+0800 OperationDemo[72184:1653790] bp2 after cancel
2018-04-18 11:44:03.598138+0800 OperationDemo[72184:1653790] bp2 before start
2018-04-18 11:44:03.598279+0800 OperationDemo[72184:1653790] bp2---isFinished---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 11:44:03.598393+0800 OperationDemo[72184:1653790] bp2 after start
在执行 -start 之前调用 -cancel 后,isCancelled 属性从NO被置为YES,isReady 属性无论什么状态都会被置为YES。这里后面讲到dependency的时候会说明。 -cancel 之后再调用 -start ,会将 isFinished 属性从NO被置为YES,然后并不调用 -main 方法。
单个Operation的行为我们已经基本了解,那么接下来我们来看一下当两个Operation添加到Queue中会是什么结果。
TestBlockOperation * bp1 = [TestBlockOperation blockOperationWithBlock:^{
NSLog(@"enter bp1");
[NSThread sleepForTimeInterval:3];
NSLog(@"leave bp1");
}];
bp1.name = @"bp1";
bp1.completionBlock = ^{
NSLog(@"bp1 complete");
};
TestBlockOperation * bp2 = [TestBlockOperation blockOperationWithBlock:^{
NSLog(@"enter bp2");
[NSThread sleepForTimeInterval:3];
NSLog(@"leave bp2");
}];
bp2.name = @"bp2";
bp2.completionBlock = ^{
NSLog(@"bp2 complete");
};
NSArray * keyPathes = @[@"isReady",@"isCancelled",@"isExecuting",@"isFinished"];
[self addObserverForOp:bp1 keyPathes:keyPathes];
[self addObserverForOp:bp2 keyPathes:keyPathes];
NSOperationQueue * q = [NSOperationQueue new];
[bp1 addDependency:bp2];
[q addOperation:bp1];
[q addOperation:bp2];
控制台输出:
2018-04-18 16:37:16.004963+0800 OperationDemo[84411:1940169] bp1 before addDependency:
2018-04-18 16:37:16.005291+0800 OperationDemo[84411:1940169] bp1---isReady---{
kind = 1;
new = 0;
old = 1;
}
2018-04-18 16:37:16.005640+0800 OperationDemo[84411:1940169] bp1 after addDependency:
2018-04-18 16:37:16.005842+0800 OperationDemo[84411:1940219] bp2 before start
2018-04-18 16:37:16.006277+0800 OperationDemo[84411:1940219] bp2---isExecuting---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:37:16.007394+0800 OperationDemo[84411:1940219] bp2 before main
2018-04-18 16:37:16.007669+0800 OperationDemo[84411:1940219] enter bp2
2018-04-18 16:37:19.010134+0800 OperationDemo[84411:1940219] leave bp2
2018-04-18 16:37:19.010351+0800 OperationDemo[84411:1940219] bp2 after main
2018-04-18 16:37:19.010701+0800 OperationDemo[84411:1940218] bp1 before start
2018-04-18 16:37:19.010707+0800 OperationDemo[84411:1940219] bp1---isReady---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:37:19.010857+0800 OperationDemo[84411:1940218] bp1---isExecuting---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:37:19.011126+0800 OperationDemo[84411:1940219] bp2---isExecuting---{
kind = 1;
new = 0;
old = 1;
}
2018-04-18 16:37:19.011134+0800 OperationDemo[84411:1940218] bp1 before main
2018-04-18 16:37:19.011143+0800 OperationDemo[84411:1940220] bp2 complete
2018-04-18 16:37:19.011229+0800 OperationDemo[84411:1940218] enter bp1
2018-04-18 16:37:19.011233+0800 OperationDemo[84411:1940219] bp2---isFinished---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:37:19.011458+0800 OperationDemo[84411:1940219] bp2 after start
2018-04-18 16:37:22.011382+0800 OperationDemo[84411:1940218] leave bp1
2018-04-18 16:37:22.011571+0800 OperationDemo[84411:1940218] bp1 after main
2018-04-18 16:37:22.012029+0800 OperationDemo[84411:1940218] bp1---isExecuting---{
kind = 1;
new = 0;
old = 1;
}
2018-04-18 16:37:22.012050+0800 OperationDemo[84411:1940219] bp1 complete
2018-04-18 16:37:22.012375+0800 OperationDemo[84411:1940218] bp1---isFinished---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:37:22.013382+0800 OperationDemo[84411:1940218] bp1 after start
当为bp1添加bp2作为依赖以后,bp1的 isReady 属性从YES置为NO。 由于bp2是bp1的依赖,所以优先执行bp2。 在bp2中任务完成之后,-main 方法调用结束之后, -start 方法调用结束之前,bp1调用 -start 并将 isReady 属性置为YES。 其他行为与单个调用时基本一致。
我们再来看看当bp1添加bp2作为依赖,并且在调用之前bp2调用 -cancel 时的状态变化,代码基本一致,唯一变化是在添加在q之前bp2调用 -cancel,我就不放代码了,直接看日志输出:
2018-04-18 16:39:38.612072+0800 OperationDemo[84462:1944038] bp1 before addDependency:
2018-04-18 16:39:38.612500+0800 OperationDemo[84462:1944038] bp1---isReady---{
kind = 1;
new = 0;
old = 1;
}
2018-04-18 16:39:38.612712+0800 OperationDemo[84462:1944038] bp1 after addDependency:
2018-04-18 16:39:38.613460+0800 OperationDemo[84462:1944038] bp2 before cancel
2018-04-18 16:39:38.613984+0800 OperationDemo[84462:1944038] bp2---isCancelled---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:39:38.614337+0800 OperationDemo[84462:1944038] bp2---isReady---{
kind = 1;
new = 1;
old = 1;
}
2018-04-18 16:39:38.614512+0800 OperationDemo[84462:1944038] bp2 after cancel
2018-04-18 16:39:38.614804+0800 OperationDemo[84462:1944152] bp2 before start
2018-04-18 16:39:38.615286+0800 OperationDemo[84462:1944158] bp1 before start
2018-04-18 16:39:38.615321+0800 OperationDemo[84462:1944152] bp1---isReady---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:39:38.615614+0800 OperationDemo[84462:1944158] bp1---isExecuting---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:39:38.615629+0800 OperationDemo[84462:1944150] bp2 complete
2018-04-18 16:39:38.615661+0800 OperationDemo[84462:1944152] bp2---isFinished---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:39:38.616030+0800 OperationDemo[84462:1944158] bp1 before main
2018-04-18 16:39:38.616115+0800 OperationDemo[84462:1944152] bp2 after start
2018-04-18 16:39:38.616132+0800 OperationDemo[84462:1944158] enter bp1
2018-04-18 16:39:41.618815+0800 OperationDemo[84462:1944158] leave bp1
2018-04-18 16:39:41.619170+0800 OperationDemo[84462:1944158] bp1 after main
2018-04-18 16:39:41.619551+0800 OperationDemo[84462:1944152] bp1 complete
2018-04-18 16:39:41.619591+0800 OperationDemo[84462:1944158] bp1---isExecuting---{
kind = 1;
new = 0;
old = 1;
}
2018-04-18 16:39:41.619941+0800 OperationDemo[84462:1944158] bp1---isFinished---{
kind = 1;
new = 1;
old = 0;
}
2018-04-18 16:39:41.620073+0800 OperationDemo[84462:1944158] bp1 after start
与单个Operation调用 -cancel 行为一致,不影响 -start 的调用,同样不会调用 -main,不同点是在bp1调用 -start 之前 isReady 属性会被置为YES,之后行为与单个Operation调用 -start 一致。
上述行为可以用一张流程图来表现:
Operation流程
通过观察上述的日志我们可以看出,当一个任务作为另一个任务的依赖时,只有当被依赖的任务完成后,才会执行另一个任务,而这个时间点的时候,executing、finished两个属性会发生变化。那我们需要做的就是实现一个NSOperation的子类,让他可以再我们需要的时候才被标记为完成状态,这样,我们只要给刷新列表任务添加网络请求任务作为依赖即可。所以,我们需要做的只有两件事,就是接过executing、finished两个属性的管理权以及在我们需要的时候改变他们的状态。
需求知道了,实现就很简单了。老司机直接就放一份简单的源码就好了。
@class DWManualOperation;
typedef void(^OperationHandler)(DWManualOperation * op);
@interface DWManualOperation : NSOperation
/**
以需要实现的任务生成operation对象
@param handler 需要实现的任务
@return operation实例
*/
+(instancetype)manualOperationWithHandler:(OperationHandler)handler;
/**
立刻将当前任务标识为完成状态,isExecuting 为 NO,isFinished 为 YES。
*/
-(void)finishOperation;
@end
@interface DWManualOperation ()
@property (nonatomic ,assign ,getter=isFinished) BOOL finished;
@property (nonatomic ,assign ,getter=isExecuting) BOOL executing;
@property (nonatomic ,copy) OperationHandler handler;
@property (nonatomic ,strong) DWManualOperation * cycleSelf;
@end
@implementation DWManualOperation
@synthesize finished = _finished;
@synthesize executing = _executing;
#pragma mark --- interface method ---
+(instancetype)manualOperationWithHandler:(OperationHandler)handler {
DWManualOperation * op = [DWManualOperation new];
if (handler) {
op.handler = handler;
}
return op;
}
-(void)finishOperation {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_finished = YES;
_executing = NO;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
#pragma mark --- override ---
-(instancetype)init {
if (self = [super init]) {
_concurrentHandler = YES;
self.completionBlock = nil;
}
return self;
}
-(void)start {
NSLog(@"start");
///如果是被取消状态则置为完成状态并返回,为了配合NSOperationQueue使用
if (self.isCancelled) {
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
if (self.isExecuting || self.isFinished) {///正在执行或已经完成的任务不可以调用开始方法。
return;
}
self.cycleSelf = self;
[super start];
}
-(void)cancel {
[super cancel];
}
-(void)main {///系统实现中 -start 方法中会调用 -main 方法
[self willChangeValueForKey:@"isExecuting"];
_executing = YES;
[self didChangeValueForKey:@"isExecuting"];
[super main];
__weak typeof(self)weakSelf = self;
if (self.handler) {
self.handler(weakSelf);
}
}
-(void)dealloc {
NSLog(@"dealloc");
}
#pragma mark --- tool func ---
static inline void freeOperation(DWManualOperation * op) {
op.cycleSelf = nil;
}
#pragma mark --- setter/getter ---
-(void)setCompletionBlock:(void (^)(void))completionBlock {
__weak typeof(self)weakSelf = self;
dispatch_block_t ab = ^(void) {
if (completionBlock) {
completionBlock();
}
freeOperation(weakSelf);
};
[super setCompletionBlock:ab];
}
@end
呐,写到这里,我们就基本实现了一个跟系统Operation具有相同行为,但是我们可以随意控制Operation是否完成的子类了。
不知道该叫什么我就随便起了个名,其实就是一个应用,场景就是操作A一定要建立在某种状态下才能执行。最简单的就是比如点赞功能必须是登录后才可进行,那么我们就要对这种状态做出判断。如下图:
条件模块
你可能说这无非就是一个判断的事,的确是,不过像登录状态这种很多地方都要用的功能这样写也能很好的复用。这个思路能主要还是借鉴的大神Delpan的这篇博客:《操作抽象设计-实践》,写的很好,同学们感兴趣可以去看看。
呐,写到这里其实就只是讲思路了,至此我们已经具有了一个可以控制完成时机的Operation了,只要我们将网络请求与Operation同时 -start 后,请求回调结束后标志Operation为完成状态后就可以为请求添加依赖了,同时也可以配合系统的其他Operation和Queue同时使用完成线程间通信。
说到这要是就结束了那就太虎头蛇尾了,而且真爱粉们应该知道,一般到这个时候就是老司机的软广环节了,着急的童鞋们可以关掉浏览器了哈~
老司机给予这个思路对AFN进行了二次封装,写了一个自用的请求框架DWFlashFlow
。
首先它具有NSOperation的所有特性,可以跟普通Operation结合在一起使用,其次我还封装了批量请求和请求量功能,并且在功能层和逻辑层上进行了分离,也就是说你可以自由更换你的请求核心类,而逻辑层不变~哎,最近都不会吹牛逼了,剩下的东西喜欢的同学自己看吧