简介
-
NSOperation、NSOperationQueue
是对GCD
的一层面向对象的封装。 -
多线程用得多的还是
GCD
,简单方便。 -
NSOperation、NSOperationQueue
有GCD
不具备的功能,比如查看状态,设置依赖,取消等等。 -
NSOperation
不能直接使用,有两个子类NSInvocationOperation、NSBlockOperation
。其中NSInvocationOperation
把任务作为selector
,用得很少,基本不考虑。NSBlockOperation
把任务当做block
,用得比较多。通过继承NSOperation
自定义main
方法,在某些特定的场合会用到。 -
NSOperationQueue、NSOperation、block
三级的概念来考虑,相对比较方便。 -
整体上来说
NSOperation、NSOperationQueue
对GCD
的封装是比较失败的。相对来说,还是GCD
的概念简洁明了。平时,也推荐多用GCD
-
NSOperationQueue
只有主队列和自定义队列两种。主队列运行在主线程之上,而自定义队列在后台执行。没有串行和并行队列的概念。有个属性叫最大并发数。maxConcurrentOperationCount
这个值为1,效果上就相当于串行队列。====从这个点就可以看出,封装的人脑子有点短路,设计太差,智商不及格。 -
NSOperation
如果不加入一个自定义队列NSOperationQueue
,那么默认就是在主线程执行,这种状况要避免。 -
一个
NSOperation
可以添加多个block
,不过最好不要这样做。一个NSOperation
只带一个block
,概念最简洁。如果业务上确实需要在某个NSOperation
上追加操作,addExecutionBlock
,可以考虑。===不过从简洁角度考虑,这始终是脱裤子放屁的坏主意。
简单使用
场景:将耗时工作放入后台工作线程;完成之后,回主线程更新界面
// 基本使用
- (IBAction)baseButtonTouched:(id)sender {
// 开始之前,修改界面
self.baseButton.enabled = NO;
NSLog(@"任务开始前,修改界面...");
// 创建队列,自定义队列默认就是并行的
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 添加耗时操作;其实是匿名operation,直接加block
[queue addOperationWithBlock:^{
NSLog(@"耗时任务开始....");
NSLog(@"%@",[NSThread currentThread]);
sleep(5);
NSLog(@"耗时任务结束");
// 切换到主线程,添加界面更新操作;其实是匿名operation,直接加block
[NSOperationQueue.mainQueue addOperationWithBlock:^{
self.baseButton.enabled = YES;
NSLog(@"更新界面");
NSLog(@"%@",[NSThread currentThread]);
}];
}];
}
企业微信截图_8ab5230f-1aa5-44aa-bb15-39e547d2b856.png队列
NSOperationQueue
可以添加匿名的operation
,直接加入了具体任务,block
死锁
-
和循环引用类似,如果存在循环依赖的情况,将会发生死锁。
-
最简单的情况:A和B两个operation,A依赖B,B依赖,就会发生死锁。
-
稍微复杂一点的情况:A、B、C三个operation,A依赖B,B依赖C,C依赖A,就会形成死锁。
-
取消一个operation,比如A,就可以打破依赖循环,破除死锁。operation是可以取消的。
// 死锁
- (IBAction)deadLockButtonTouched:(id)sender {
// 设置操作
NSBlockOperation *operationA = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务A开始....");
NSLog(@"%@",[NSThread currentThread]);
sleep(3);
NSLog(@"任务A结束");
}];
NSBlockOperation *operationB = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务B开始....");
NSLog(@"%@",[NSThread currentThread]);
sleep(1);
NSLog(@"任务B结束");
}];
NSBlockOperation *operationC = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务C开始....");
NSLog(@"%@",[NSThread currentThread]);
sleep(5);
NSLog(@"任务C结束");
}];
// 添加依赖关系;这里B依赖A,C依赖B,A依赖C,循环依赖,造成死锁
[operationB addDependency:operationA];
[operationC addDependency:operationB];
[operationA addDependency:operationC];
// 将操作加入队列;任务会自动启动
NSOperationQueue *workQueue = [[NSOperationQueue alloc] init];
[workQueue addOperations:@[operationA, operationB, operationC] waitUntilFinished:NO];
NSLog(@"调度完毕,主线程空闲。。。。");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"等了6s都没完成,估计发生死锁了。手动停掉一个");
[operationA cancel];
});
}
企业微信截图_7412d842-766a-48d2-a80a-b9b5aac03f14.png
多读单写
-
每个读写操作,都当做一个
block
,也就是一个任务; -
所有写之前的读操作,都集中放入一个
readOperationBefore
-
写操作,单独放入一个
writeOperation
-
所有些之后的读操作,都集中放入一个
readOperationAfter
-
通过依赖关系,保证多读单写的顺序。
readOperationBefore
<=writeOperation
<=readOperationAfter
// 多读单写
- (IBAction)readWriteButtonTouched:(id)sender {
// 写之前的读操作
NSBlockOperation *readOperationBefore = [[NSBlockOperation alloc] init];
[readOperationBefore addExecutionBlock:^{
NSLog(@"写之前的读操作1开始....");
sleep(1);
NSLog(@"写之前的读操作1结束");
}];
[readOperationBefore addExecutionBlock:^{
NSLog(@"写之前的读操作2开始....");
sleep(1);
NSLog(@"写之前的读操作2结束");
}];
// 写操作
NSBlockOperation *writeOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"写操作开始....");
sleep(2);
NSLog(@"写操作结束");
}];
// 写之后的读操作
NSBlockOperation *readOperationAfter = [[NSBlockOperation alloc] init];
[readOperationAfter addExecutionBlock:^{
NSLog(@"写之后的读操作1开始....");
sleep(1);
NSLog(@"写之后的读操作1结束");
}];
[readOperationAfter addExecutionBlock:^{
NSLog(@"写之后的读操作2开始....");
sleep(1);
NSLog(@"写之后的读操作2结束");
}];
// 添加依赖关系;
[writeOperation addDependency:readOperationBefore];
[readOperationAfter addDependency:writeOperation];
// 将操作加入队列;任务会自动启动
NSOperationQueue *workQueue = [[NSOperationQueue alloc] init];
[workQueue addOperations:@[readOperationBefore, writeOperation, readOperationAfter] waitUntilFinished:NO];
}
企业微信截图_d9dd1a86-6184-4845-b48f-ade6e080c7f6.png
任务组
-
将所有子任务都当做一个一个的
block
,加入一个workOperation
-
将更新界面的操作放入
mainOperation
-
设置依赖关系:
workOperation
<=mainOperation
-
将
workOperation
加入一个并行队列,普通队列,NSOperationQueue
;
将mainOperation
放入主队列NSOperationQueue.mainQueue
// 任务组
- (IBAction)groupButtonTouched:(id)sender {
// 网络任务开始前,修改界面
self.groupButton.enabled = NO;
NSLog(@"网络组下载任务开始了... ...");
// 将所有的下载子任务都放入一个operation
NSBlockOperation *workOperation = [[NSBlockOperation alloc] init];
[workOperation addExecutionBlock:^{
NSLog(@"任务1开始...");
sleep(2);
NSLog(@"任务1结束");
}];
[workOperation addExecutionBlock:^{
NSLog(@"任务2开始...");
sleep(1);
NSLog(@"任务2结束");
}];
[workOperation addExecutionBlock:^{
NSLog(@"任务3开始...");
sleep(3);
NSLog(@"任务3结束");
}];
// 更新界面操作放入一个operation
NSBlockOperation *mainOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"完成所有子任务,更新界面");
self.groupButton.enabled = YES;
}];
// 添加依赖关系
[mainOperation addDependency:workOperation];
// 将操作加入队列;任务会自动启动
NSOperationQueue *workQueue = [[NSOperationQueue alloc] init];
[workQueue addOperation:workOperation];
[NSOperationQueue.mainQueue addOperation:mainOperation];
}
企业微信截图_741ce1ed-b61c-493f-a2c7-54b65e1bbf2a.png
线程安全
-
NSOperationQueue
没有线程安全相关的属性或者方法 -
线程安全可以借助
NSLock
实现 -
场景模拟:上海,北京,杭州三个站一起开始卖火车票
-
卖票过程是需要线程安全的。对于票数
ticketNumber
,无论读写都要求线程安全。
// 卖票;name:售票窗口名; number:售票数量
- (void)saleTicketWithName:(NSString *)name andNumber:(NSInteger)number {
// 加锁,保护self.ticketNumber
[self.lock lock];
if (self.ticketNumber >= number) {
// 售票时间
[NSThread sleepForTimeInterval:0.5];
self.ticketNumber -= number;
NSLog(@"%@窗口售出%ld张火车票,剩余票数:%ld", name, (long)number, (long)self.ticketNumber);
} else {
NSLog(@"%@窗口售票%ld张失败,剩余票数:%ld", name, (long)number, (long)self.ticketNumber);
}
// 解锁
[self.lock unlock];
}
- 卖票过程是并行的,更新界面需要在主线程中,这里存在依赖关系
saleOperation <= uiOperation
// 线程安全
- (IBAction)safeButtonTouched:(id)sender {
// 开始售票
self.safeButton.enabled = NO;
self.ticketNumber = 30;
NSLog(@"售票开始了,一共有%ld张票", (long)self.ticketNumber);
// 所有的售票活动都放入saleOperation
NSBlockOperation *saleOperation = [[NSBlockOperation alloc] init];
// 售票点1
[saleOperation addExecutionBlock:^{
for (int i = 0; i < 4; i++) {
NSInteger number = 1 + arc4random() % 5;
[self saleTicketWithName:@"上海站" andNumber:number];
[NSThread sleepForTimeInterval:1];
}
}];
// 售票点2
[saleOperation addExecutionBlock:^{
for (int i = 0; i < 3; i++) {
NSInteger number = 1 + arc4random() % 5;
[self saleTicketWithName:@"北京站" andNumber:number];
[NSThread sleepForTimeInterval:1];
}
}];
// 售票点3
[saleOperation addExecutionBlock:^{
for (int i = 0; i < 3; i++) {
NSInteger number = 1 + arc4random() % 5;
[self saleTicketWithName:@"杭州站" andNumber:number];
[NSThread sleepForTimeInterval:1];
}
}];
// 界面更新放入uiOperation
NSBlockOperation *uiOperation = [NSBlockOperation blockOperationWithBlock:^{
self.safeButton.enabled = YES;
NSLog(@"售票结束,剩余%ld张票", (long)self.ticketNumber);
}];
// 建立依赖; 先售票,再更新界面
[uiOperation addDependency:saleOperation];
// 添加队列,并启动
NSOperationQueue *saleQueue = [[NSOperationQueue alloc] init];
[saleQueue addOperation:saleOperation];
[NSOperationQueue.mainQueue addOperation:uiOperation];
}
企业微信截图_e6257b67-2997-4917-a5da-b73167e709dc.png
自定义NSOPeration
NSBlockOperation
已经足够好用,实在有需要,可以考虑自定义NSOPeration
吐槽
-
从整体设计角度来讲,比较差,原因是范围过大,结构复杂,比较难理解。
-
分为队列,操作,任务三级,层级较多。
-
任务又分为
block,Selector,
自定义三种,过于复杂。 -
以最大并发数量为1来表示串行队列,不够直观。
小结
-
尝试过之后,确实是在
GCD
的基础上进行了封装,减少了很多细节 -
任务固定为
block
,放弃Selector
和自定义,大多数情况下是可行的。只要用好block
就可以了,这样就简单直接了。 -
队列就分为自定义的并行队列和全局队列两种,这样就简单了。除了更新界面的事情,其他都放自定义的并行队列中。只有一些特殊场景,比如两个写操作,需要用到串行队列,那么就把队列的最大并发数量设置为1,其他情况,都不需要管这个属性。
-
有顺序要求,就考虑依赖,基本上可以解决运行顺序问题。
-
线程安全,就引入
NSLock
,这个用起来也简单。 -
简化之后,多线程就考虑用
NSOperation、NSOperationQueue-
,这个比GCD
用起来方便,功能也多一点。
参考文章
iOS 多线程:『NSOperation、NSOperationQueue』详尽总结
网友评论