iOS多线程:NSOperation和GCD对比以及各种锁的测试

测试代码MultiThread

NSOperation和GCD对比

两者的对比,区别在一下这些方面:

  • 任务之间添加依赖关系的不同
  • NSOperation可以监控任务的各种状态并且可以实现取消
  • NSOperation可以进行继承和封装,从而实现代码重用,并且逻辑规整。
  • GCD使用更快速方便,使用于简单临时的任务


1. 添加依赖
1.1 NSOperation的依赖

首先实现一个自定义的NSOperation,头文件:

#import 

@interface TFTestOperation : NSOperation

@property (nonatomic, copy) NSString *taskPath;

@property (nonatomic, copy) void(^exeCompleteHandler)(NSData *data);

@property (nonatomic, copy) void(^freeHandler)(TFTestOperation *freeOperation);

@end

实现文件:

#import "TFTestOperation.h"

@interface TFTestOperation (){
    NSURLSessionTask *_task;
}

@property (atomic, assign) BOOL taskExecuting;
@property (atomic, assign) BOOL taskFinished;

@end

@implementation TFTestOperation

-(void)start{
    
    NSURL *url = [[NSURL alloc] initWithString:_taskPath];
    if (url == nil) {
        _exeCompleteHandler(nil);
        return;
    }
    NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:100];
    _task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        if (error) {
            NSLog(@"%@",error);
            
        }
        
        [self willChangeValueForKey:@"isExecuting"];
        [self willChangeValueForKey:@"isFinished"];
        _taskExecuting = NO;
        _taskFinished = YES;
        NSLog(@"%@ data length: %lu",self.name,data.length);
        [self didChangeValueForKey:@"isExecuting"];
        [self didChangeValueForKey:@"isFinished"];
        _exeCompleteHandler(data);
    }];
    [_task resume];
    
    _taskExecuting = YES;
    NSLog(@"operation %@ started",self.name);
}

-(void)cancel{
    [super cancel];
    [_task cancel];
}

-(BOOL)isAsynchronous{
    return YES;
}

-(BOOL)isExecuting{
    return _taskExecuting;
}

-(BOOL)isFinished{
    return _taskFinished;
}

-(void)dealloc{
    if (self.freeHandler) {
        self.freeHandler(self);
    }
}

然后测试任务之间的依赖:

-(void)testDependency_operation{
    TFTestOperation *operation1 = [[TFTestOperation alloc] init];
    operation1.taskPath = kImagePath1;
    operation1.name = @"operation1";
    operation1.exeCompleteHandler = ^(NSData *data) {
        dispatch_async(dispatch_get_main_queue(), ^{
            _firstImgView.image = [UIImage imageWithData:data];
        });
    };
    
    TFTestOperation *operation2 = [[TFTestOperation alloc] init];
    operation2.name = @"operation2";
    operation2.taskPath = kImagePath2;
    operation2.exeCompleteHandler = ^(NSData *data) {
        dispatch_async(dispatch_get_main_queue(), ^{
            _secondImgView.image = [UIImage imageWithData:data];
        });
    };
    
    [operation2 addDependency:operation1];
    
    //NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//    [queue addOperation:operation1];
//    [queue addOperation:operation2];
    
    _operationQueue = [[NSOperationQueue alloc] init];
    [_operationQueue addOperation:operation1];
    [_operationQueue addOperation:operation2];
    
    
}

TFTestOperation实际就干了一个下载文件的任务,这里operation1operation2都下载一个图片,但是通过[operation2 addDependency:operation1];让任务2依赖任务1,这样在operation1执行完之后operation2才会执行。

但是什么时候operation1执行完成?
这就要回到TFTestOperation的实现里去,有个-(BOOL)isFinished的方法,就是它起作用。但是这个方法是不会自己调用的,在你的任务结束的时候,你需要通知外界,你的任务完成了。

所以在下载完成后有[self didChangeValueForKey:@"isFinished"];等一长串代码,使用KVO的手段通知外界。

我的猜测是:当ope2依赖了ope1,那么ope2就会对ope1的isFinished属性进行KVO的监听,当ope1发出通知isFinished改变,那么ope2得知后,就会去调用ope1的-(BOOL)isFinished方法,如果真的完成了,那么就轮到它执行了。

这也是为什么文档里指出isFinished是必须被重载的一个属性吧。

如果是多个依赖,也就是加多个dependency的问题,本质不变。


GCD的依赖

在我看来,“依赖关系”就是一个任务需要等另一个任务结束才执行,或者等另外n个任务结束才执行。对于GCD里,我想到的就是dispatch_group_xxx这系列的方法了

-(void)testMultiDependencies_GCD{
    dispatch_group_t group = dispatch_group_create();
    
    int count = 100;
    for (int i = 0; i
  • 新建一个dispatch_group_t
  • 在一个任务开始的时候dispatch_group_enter(group)
  • 在一个任务结束的时候dispatch_group_leave(group)
  • 把最后想要执行的任务放在dispatch_group_notify里,当所有的enter和leave对等的时候,就会执行。

如果只是一次依赖,那么dispatch_group也不麻烦,就怕多个依赖,比如A依赖B,B又依赖C,C再依赖D和F,这对于NSOperation来说就是很简单的事,但对GCD就麻烦了。

  • 每一个依赖环节都需要一个group,需要一系列的处理
  • 然后一个致命的问题是,A依赖B,group leave的代码却需要插入到B的逻辑代码里面去,假如你已经写好了A依赖B,又来个C依赖B,那么又要去动B的代码,而NSOperation就化解了这个问题:B执行完成就发个KVO通知,需要知道完没完成的自己去检查吧。这样就把问题丢回给你依赖者,它自身的部分可以保持恒定不变。


2. 对线程任务的状态监测和控制

NSOperation可以监测状态,但GCD没有;NSOperation可以取消,而GCD只能暂停没执行的任务。

2.1 NSOperation状态监测

使用KVO监测:

-(void)testStateControl_operation{
    _operationQueue = [[NSOperationQueue alloc] init];
    
    TFTestOperation *operation1 = [[TFTestOperation alloc] init];
    operation1.taskPath = kFilePath1;
    operation1.name = @"1";
    operation1.exeCompleteHandler = ^(NSData *data) {
        NSLog(@"file download finished, data size:%ld",data.length);
        
    };
    
    operation1.freeHandler = ^(TFTestOperation *freeOperation) {
        [freeOperation removeObserver:self forKeyPath:@"isFinished"];
        [freeOperation removeObserver:self forKeyPath:@"isExecuting"];
        [freeOperation removeObserver:self forKeyPath:@"isReady"];
    };
    
    [operation1 addObserver:self forKeyPath:@"isFinished" options:NSKeyValueObservingOptionNew context:nil];
    [operation1 addObserver:self forKeyPath:@"isExecuting" options:NSKeyValueObservingOptionNew context:nil];
    [operation1 addObserver:self forKeyPath:@"isReady" options:NSKeyValueObservingOptionNew context:nil];
    
    
    
    TFTestOperation *operation2 = [[TFTestOperation alloc] init];
    operation2.taskPath = kImagePath1;
    operation2.name = @"2";
    operation2.exeCompleteHandler = ^(NSData *data) {
        NSLog(@"file download finished, data size:%ld",data.length);
        
    };
    
    operation2.freeHandler = ^(TFTestOperation *freeOperation) {
        [freeOperation removeObserver:self forKeyPath:@"isFinished"];
        [freeOperation removeObserver:self forKeyPath:@"isExecuting"];
        [freeOperation removeObserver:self forKeyPath:@"isReady"];
    };
    
    [operation2 addObserver:self forKeyPath:@"isFinished" options:NSKeyValueObservingOptionNew context:nil];
    [operation2 addObserver:self forKeyPath:@"isExecuting" options:NSKeyValueObservingOptionNew context:nil];
    [operation2 addObserver:self forKeyPath:@"isReady" options:NSKeyValueObservingOptionNew context:nil];

    [operation2 addDependency:operation1];
    
    [_operationQueue addOperation:operation1];
    [_operationQueue addOperation:operation2];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"cancel");
        [operation1 cancel];
    });
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    
    if ([object isKindOfClass:[NSOperation class]]) {
        
        NSLog(@"%@ %@: %@",((NSOperation *)object).name,keyPath,[[change objectForKey:NSKeyValueChangeNewKey] boolValue]?@"YES":@"NO");
    }
    
}
  • 建了operation1和operation2两个任务执行,并且operation2依赖operation1执行完成。
  • 在任务开始3秒后把operation1取消掉
  • 观察的keyPath不是属性名,而是getter方法名称
  • 通过log可以看到3个值的变化,[operation1 cancel]取消operation1后,operation2立马执行了。

关于使用KVO的一个注意点,operation执行完,它就被释放了,然后self还Observer着它,自然会奔溃,而且这个释放点外界很难抓。所以我做了个处理,给TFTestOperation添加了一个freeHandler,就是在dealloc的时候调用的,让观察者释放。

最后,cancel是需要自己重写的,根据NSOperation的具体业务来做处理,这里测试使用的是文件下载,那么只需把下载的SessionTask取消即可,不同的任务有不同的可能。

2.2 GCD的任务取消
-(void)testStateControl_GCD{
    dispatch_queue_t queue = dispatch_queue_create("GCD_stateControl", DISPATCH_QUEUE_SERIAL);
    
    for (int i = 0; i<10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"task start%d",i);
            sleep(1);
            NSLog(@"task%d",i);
        });
    }
    
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        dispatch_suspend(queue);
        NSLog(@"suspend");
        sleep(5);
        
        NSLog(@"sleep finished");
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            dispatch_resume(queue);
            NSLog(@"resume");
        });
        
    });
}
  • 在一个串行队列里新建了10个任务
  • 在2秒后暂停这个队列
  • 在5秒后恢复

为什么是串行队列,因为如果选用并行队列,在建立任务的一瞬间,他们就各自开始执行了,dispatch_suspend对已经执行的任务是不起作用的。使用串行队列,因为任务是一个接一个的执行,那么2秒后,还有任务在等待中,他们就被暂停了。

在2秒后暂停,可以看到,有些任务执行了,而有的任务要等一会,这样可以看出差距。

最后很坑爹的:

By suspending a dispatch object, your application can temporarily prevent the execution of any blocks associated with that object. The suspension occurs after completion of any blocks running at the time of the call. Calling this function increments the suspension count of the object, and calling dispatch_resume decrements it. While the count is greater than zero, the object remains suspended, so you must balance each dispatch_suspend
call with a matching dispatch_resume
call.

  • 注意temporarily暂时阻止执行
  • 然后dispatch_suspenddispatch_resume必须平衡,就是暂停了后面一定要恢复。试过不恢复,奔溃。
  • 更神奇的是
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            dispatch_resume(queue);
            NSLog(@"resume");
        });

这种在n秒之后还没执行的代码,有dispatch_resume就没问题,没有就奔溃,鬼知道苹果做了什么处理。


总得来说NSOperation的控制力度要大得多,而GCD像是为了一次性任务而生,爽快的来爽快的去!


各种锁

这部分我很喜欢,说实话这里体现了一些很有意思的思想。

  • 互斥锁: NSLock
  • 递归锁:NSRecursiveLock
  • 条件锁:NSCondition
  • 信号量:dispatch_semaphore_t
  • 读写锁:dispatch_barrier_sync,这个比较特殊,不是锁,但是可以是现实读写锁的需求
1. NSLock

为了模拟情况,新建了一个资源类:
头文件

@interface TFResource : NSObject

+(instancetype)shareInstance;

@property (nonatomic, assign) NSInteger count;

-(void)addSomeResources;

-(void)useOne;

@end

实现代码

@implementation TFResource

+(instancetype)shareInstance{
    static TFResource *dataSource = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dataSource = [[TFResource alloc] init];
    });
    
    return dataSource;
}

-(instancetype)init{
    if (self = [super init]) {
        _count = 100;
    }
    
    return self;
}

-(void)addSomeResources{
    int count = arc4random() % 10;
    self.count += count;
    
    NSLog(@"====================gen :%d",count);
}

-(void)useOne{
    self.count = self.count - 1;
    
}

其实就维持一个数量,然后useOne数量减1,addSomeResources增加10以内随机个数。这是模拟了跟经典的卖票问题一样的情况。

1.1 首先来个不加锁的反例:
-(void)noSyncNSLocking{
    dispatch_queue_t queue = dispatch_queue_create("resourceUseQueue", DISPATCH_QUEUE_CONCURRENT);
    
    int taskCount = 10;
    for (int i = 0; i 0) {
                NSLog(@"taskUse%d, <<<%ld",i,(long)_resource.count);
                sleep(arc4random()%100/1000);
                [_resource useOne];
            }
            
            NSLog(@"task%d end, real: %ld",i,(long)_resource.count);
        });
    }
}

开10个队列,只要有票,就useOne,等没票了,输出当前实际的票数。然后结果就是左后票数变成了-9。


1.2 然后只对使用加锁,但是对检查不加锁
-(void)NSLock_DontLockCheckCountNSLocking{
    dispatch_queue_t queue = dispatch_queue_create("resourceUseQueue", DISPATCH_QUEUE_CONCURRENT);
    
    int taskCount = 10;
    for (int i = 0; i 0) {
                NSLog(@"taskEnter%d,   >>>%ld",i,(long)_resource.count);
                [_resourceLock lock];
                
                NSLog(@"taskUse%d, <<<  %ld",i,(long)_resource.count);
                [_resource useOne];
                
                [_resourceLock unlock];
            }
            
            NSLog(@"task%d end, real: %ld",i,(long)_resource.count);
        });
    }
}

while (_resource.count > 0)检查是否可执行的时候,没有加锁,那么结果有没有问题呢?答案是最后数量依然是-9。

这个情形我思考了很久,是一个收获点。问题出在哪里呢?

假设现在有个房间,放着资源,然后10个人去搬。某一刻,房间里有5个资源,然后10个人都看到了,每个人心里都想,(a)还有5个,还可以去拿。然后他们都去了,虽然他们(b)一次排队,并且每次只有一个人进去搬,上一个人出来了,后一个人再进去。最后还是出现有人进了房间没有东西。

(a)这里的逻辑就是代码里的_resource.count > 0,(b)这里的逻辑就是对[_resource useOne];加锁。虽然使用加锁,但判断没有,那么判断就会失误,导致过多的执行使用。

要保证资源使用的唯一,就必须:从检查到使用,都必须唯一。所以代码就成了:

-(void)NSLock_lockAllNSLocking{
    dispatch_queue_t queue = dispatch_queue_create("resourceUseQueue", DISPATCH_QUEUE_CONCURRENT);
    
    int taskCount = 10;
    for (int i = 0; i>>%ld",i,(long)_resource.count);
                [_resourceLock lock];
                
                NSLog(@"taskUse%d, <<<  %ld",i,(long)_resource.count);
                if (_resource.count > 0) {
                    [_resource useOne];
                }else{
                    break;
                }
                
                [_resourceLock unlock];
            }
            
            NSLog(@"task%d end, real: %ld",i,(long)_resource.count);
        });
    }
}


2. NSCondition

上面的情形是,资源总是是固定,用完就结束,但还有种情况,资源同时在生产和被消耗。也就是,如果资源没了,不是结束,而是等一等,等待资源又有了,又开始使用。

如果用NSLock实现,我想到的是这样:

-(void)NSLock_WaitTestNSLocking{
    dispatch_queue_t queue = dispatch_queue_create("resourceUseQueue", DISPATCH_QUEUE_CONCURRENT);
    
    __block int addTimes = 10;
    
    dispatch_async(queue, ^{
        
        while (addTimes > 0) {
            [_resourceLock lock];
            
            [_resource addSomeResources];
            
            [_resourceLock unlock];
            
            addTimes --;
            
            sleep(arc4random()%3);
        }
    });
    
    //如果不使用NSCondition,那么在等待资源恢复的时候,就是要不断的去检查,而且这个过程伴随不断的加锁、解锁。
    //NSCondition的的逻辑相当于,所有人都在门外等,然后派一个人去看着,一旦有资源来了,就唤醒所有的等待者。就是“等待-唤醒”的模式。
    int taskCount = 10;
    for (int i = 0; i>>%ld",i,(long)_resource.count);
                [_resourceLock lock];
                
                NSLog(@"taskUse%d, <<<  %ld",i,(long)_resource.count);
                if (_resource.count > 0) {
                    [_resource useOne];
                }else{
                    [_resourceLock unlock];
                    continue;
                }
                
                [_resourceLock unlock];
            }
        });
    }
}
  • 使用一个线程循环的增加资源,每次随机0-9个
  • 开另外10个线程来消耗资源,每次1个
  • 在资源为空时,释放锁,然后从新开始判断,如此循环。

注意到第三步是一个浪费的操作,因为不断的加锁、解锁,而且线程完全不停歇。当然也可以加个sleep稍微让线程停歇下。但NSCondition可以完美解决这些问题。

-(void)NSCondition_WaitTestNSLocking{
    dispatch_queue_t queue = dispatch_queue_create("resourceUseQueue", DISPATCH_QUEUE_CONCURRENT);
    
    __block int addTimes = 30;
    
    int taskCount = 10;
    for (int i = 0; i 0) {
            [_resourceCondition lock];
            
            [_resource addSomeResources];
            
            //每句只会唤醒一个线程
            [_resourceCondition signal];
//            [_resourceCondition signal];
//            [_resourceCondition signal];
//            [_resourceCondition signal];

            sleep(2);
            NSLog(@"unlock add task");
            [_resourceCondition unlock];
            
            addTimes --;
            
            sleep(arc4random()%3);
        }
    });
}
  • NSCondition和互斥锁一样,只是多了检查。当条件不满足(资源为空)时,进入等待[_resourceCondition wait],而不需要不断的循环判断,它会自动解锁并阻塞线程

  • 在其他线程,如果发现条件可能满足(增加了新资源),就释放[_resourceCondition signal];来唤醒一个等待的线程。这样等待的线程会被唤醒然后去检查。

  • 如果做个形象的比喻:
    NSLock的不断循环检查,就相当于人不断的去仓库查看是不是有货,如果有10个搬运工,那么就10个人都去看,然后又回来,而且还是一个个的依次进仓库,还不是同时进。
    NSCondition的的逻辑相当于,所有人都在门外等,然后派一个人去看着,一旦有资源来了,就唤醒所有的等待者。

  • 注意点1:[_resourceCondition signal];每次只会唤醒一个等待线程,如果同时调用了多次,那么就会同时有多个等待者进行竞争。所以要在[_resourceCondition wait];外层加个while判断条件,就是唤醒了依然可能拿不到资源,比如有3个新资源,然后唤醒了4个线程,那么最后一个其实还是拿不到。也就是“唤醒”!="条件满足"。

  • 注意点2:[_resourceCondition wait];干的事逻辑上是unlock-wait-signal-reAcquireLock-return,也就是进前等待钱它会解锁,唤醒后,会自动重新获取锁,只有重新获取锁了,才会return这个方法,也就是表面上看起来,它从来没有释放锁,只是睡了一会儿。
    这是我从另一个线程lock时成功以及文档的解释得出的。文档说的还算清楚

    When a thread waits on a condition, the condition object unlocks its lock and blocks the thread. When the condition is signaled, the system wakes up the thread. The condition object then reacquires its lock before returning from the wait or waitUntilDate: method. Thus, from the point of view of the thread, it is as if it always held the lock.

NSRecursiveLock

递归锁比较容易理解,就是互斥锁的一个特例,已经获取锁的线程可以多次获取这个锁,而其他线程不能。

-(void)NSRecursiveLockNSLocking{
    //use NSLock --> dead lock
    dispatch_queue_t queue1 = dispatch_queue_create("resourceUseQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("resourceUseQueue", DISPATCH_QUEUE_SERIAL);
    
//    dispatch_async(queue1, ^{
//        [_resourceLock lock];
//        
//        [_resourceLock lock];
//        NSLog(@"enter inner");
//        if (_resource.count > 0) {
//            NSLog(@"use one");
//            [_resource useOne];
//        }
//        NSLog(@"inner unlock");
//        [_resourceLock unlock];
//
//        
//        NSLog(@"outer unlock");
//        [_resourceLock unlock];
//    });
    
    //use rcursive lock to acquire lock muiltiple times for the same thread
    //自身线程可重复获取锁,但是仍然必须lock和unlock对等,即lock之后就要有匹配的unlock,否则一直控制锁,其他线程就进不来
    dispatch_async(queue1, ^{
        [_recursiveLock lock];
        
        [_recursiveLock lock];
        NSLog(@"1:enter inner");
        if (_resource.count > 0) {
            NSLog(@"1:use one");
            [_resource useOne];
        }
        NSLog(@"1:inner unlock");
        [_recursiveLock unlock];
        
        
        NSLog(@"1:outer unlock");
        sleep(1);
        [_recursiveLock unlock];
    });
    
    dispatch_async(queue2, ^{
        [_recursiveLock lock];
        NSLog(@"2:enter inner");
        if (_resource.count > 0) {
            NSLog(@"2:use one");
            [_resource useOne];
        }
        NSLog(@"2:inner unlock");
        [_recursiveLock unlock];
    });

}
  • 注释部分就是使用NSLock时,自身线程重读获取锁,结果就是dead lock

信号量和读写锁有时间再续。。。

你可能感兴趣的:(iOS多线程:NSOperation和GCD对比以及各种锁的测试)