iOS线程相关iOSiOS技术点

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

2017-05-17  本文已影响114人  FindCrt

测试代码MultiThread

NSOperation和GCD对比

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

<br />

1. 添加依赖
1.1 NSOperation的依赖

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

#import <Foundation/Foundation.h>

@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的问题,本质不变。

<br />

GCD的依赖

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

-(void)testMultiDependencies_GCD{
    dispatch_group_t group = dispatch_group_create();
    
    int count = 100;
    for (int i = 0; i<count; i++) {
        
        dispatch_group_enter(group);
        
        NSString *taskPath = kImagePath2;
        NSURL *url = [[NSURL alloc] initWithString:taskPath];
        NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:100];
        NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            
            if (error) {
                NSLog(@"%@",error);
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                _firstImgView.image = [UIImage imageWithData:data];
                NSLog(@"image %d",i);
            });
            dispatch_group_leave(group);
        }];
        
        [task resume];
    }
    
    dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
        NSString *taskPath = kImagePath1;
        NSURL *url = [[NSURL alloc] initWithString:taskPath];
        NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:100];
        NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            
            if (error) {
                NSLog(@"%@",error);
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                _secondImgView.image = [UIImage imageWithData:data];
            });
        }];
        
        [task resume];
    });
    
}

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

<br />

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<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    if ([object isKindOfClass:[NSOperation class]]) {
        
        NSLog(@"%@ %@: %@",((NSOperation *)object).name,keyPath,[[change objectForKey:NSKeyValueChangeNewKey] boolValue]?@"YES":@"NO");
    }
    
}

关于使用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");
        });
        
    });
}

为什么是串行队列,因为如果选用并行队列,在建立任务的一瞬间,他们就各自开始执行了,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.

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就没问题,没有就奔溃,鬼知道苹果做了什么处理。

<br />

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

<br />

各种锁

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

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<taskCount; i++) {
        dispatch_async(queue, ^{
            while (_resource.count > 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。

<br />

1.2 然后只对使用加锁,但是对检查不加锁
-(void)NSLock_DontLockCheckCountNSLocking{
    dispatch_queue_t queue = dispatch_queue_create("resourceUseQueue", DISPATCH_QUEUE_CONCURRENT);
    
    int taskCount = 10;
    for (int i = 0; i<taskCount; i++) {
        dispatch_async(queue, ^{
            while (_resource.count > 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<taskCount; i++) {
        dispatch_async(queue, ^{
            while (1) {
                NSLog(@"taskEnter%d,   >>>%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);
        });
    }
}

<br />

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<taskCount; i++) {
        dispatch_async(queue, ^{
            while (1) {
                NSLog(@"taskEnter%d,   >>>%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];
            }
        });
    }
}

注意到第三步是一个浪费的操作,因为不断的加锁、解锁,而且线程完全不停歇。当然也可以加个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<taskCount; i++) {
        dispatch_async(queue, ^{
            while (1) {
                [_resourceCondition lock];
                
                while (_resource.count <= 0) {
                    NSLog(@"taskWait:%d",i);
                    
                    //这个方法在signal之后会自动获取锁,只有获取了锁才会返回。开始调用的时候,会释放锁。也就相当于:unlock-wait-signal-reAcquireLock-return
                    //这样的流程保证不会死锁,多个同时唤醒依然保证线程同步
                    //wait需要嵌套在while里面是因为“被唤醒”!=“条件满足”,因为可能同时释放多个锁
                    [_resourceCondition wait];
                    NSLog(@"taskWakeUp:%d",i);
                }
                
                NSLog(@"taskUse%d, <<<  %ld",i,(long)_resource.count);
                [_resource useOne];
                
                NSLog(@"unlock%d",i);
                [_resourceCondition unlock];
            }
        });
    }
    
    dispatch_async(queue, ^{
        
        while (addTimes > 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);
        }
    });
}
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];
    });

}

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

上一篇 下一篇

猜你喜欢

热点阅读