Operation-Queues 并发编程

901 阅读15分钟
原文链接: ppsheep.com

并发、异步在我们的编程中,见到的太多了。在iOS中,实现并发的主要三个途径Operation Queues、Dispatch Queues、Dispatch Sources,今天我们就来详细介绍Operatin Queues的使用,花了两天时间写这一篇,值得一看。

为什么要并发

现在是个手机,拿出来就是多少多少CPU,并且这个CPU指挥增加不会减少,那我们作为开发者,需要尽可能地利用这么多个核心数,怎么利用呢,就是提高并发。

GCD和Operation Queues的选择

经常看到这样的问题,到底是使用队列还是GCD呢,我说一下我在平常编码过程中的使用情况。我一般GCD是只使用在写单例的时候,其他需要并发异步的地方,我一般使用的是队列。

因为队列的可控性更强一些,苹果提供了一系列的API供我们操作并发的任务。虽然队列是在GCD的基础上再封装一层,这样说来使用队列性能会较GCD差一些,但是这个性能差异早已经不是我们该考虑的问题,这个差异无外乎就是Operation对象的开销,这个差异就太小了。

GCD和Operation Queues的区别

使用Operation Queues,我们队任务的可控性会强很多,对于一个正在执行的任务,我们可以暂停、恢复、取消,还可以为任务之间添加依赖

但是对于GCD呢,我们不关心任务的调度情况,只要扔进去,让系统帮我们处理就好,如果我们要实现像上面队列所说的,就非常棘手

Operation(NSOperation)

我们一般需要并发任务的时候,都是将任务封装到一个operation对象中,但是我们去看苹果官方的文档,上面有这样一句话

The NSOperation class is an abstract class you use to encapsulate the code and data associated with a single task. Because it is abstract, you do not use this class directly but instead subclass or use one of the system-defined subclasses (NSInvocationOperation or BlockOperation) to perform the actual task. Despite being abstract, the base implementation of NSOperation does include significant logic to coordinate the safe execution of your task. The presence of this built-in logic allows you to focus on the actual implementation of your task, rather than on the glue code needed to ensure it works correctly with other system objects.

解释一下,上面的大致意思就是Operation(在Swift中已经更名为Operation,在OC中为NSOperation,上面一段话其实是Swift下的一个文档,可能苹果还没及时更新)其实是一个抽象类,不能直接实例化,我们需要自定义一个它的子类或者使用系统提供的NSInvocationOperation或者BlockOperation

注意:在Swift中 NSInvocationOperation 已经被抛弃掉了,只剩下BlockOperation,在OC下 NSInvocationOperation 仍然可以使用

NSInvocationOperation

使用NSInvocationOperation能够非常方便的并发任务,在NSInvocationOperation中有这样一个方法

- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;

加入我们需要并发这个方法中的代码,那么我们只需要传入方法所在的对象和这个现有的方法,就可以直接实行并发

我们来看一下简单的代码

@interface ViewController ()
@property (nonatomic, strong) NSOperationQueue *myQueue;
@end
@implementation ViewController
-(void)viewDidLoad{
    [super viewDidLoad];
    self.myQueue = [[NSOperationQueue alloc] init];
    NSInvocationOperation *invocationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doSomething:) object:@"data"];
    [self.myQueue addOperation:invocationOp];
    NSLog(@"%@",[NSThread currentThread]);
}
- (void)doSomething:(id)data{
    NSLog(@"%@-%@-%@",data,[NSThread mainThread],[NSThread currentThread]);
    sleep(3);
    NSLog(@"finish");
}
@end

非常简单,上面的代码就是讲方法doSomething:异步执行,我们看一下控制台输出什么?

2017-03-14 15:40:37.520 OperationQueues[10294:2193908] {number = 1, name = main}
2017-03-14 15:40:37.520 OperationQueues[10294:2194320] data-{number = 1, name = (null)}-{number = 3, name = (null)}
2017-03-14 15:40:40.525 OperationQueues[10294:2194320] finish

从上面的输出,我们可以看出,doSomething:这个方法子啊线程0x600000266440中执行,而我们拿到的主线程是0x600000079500,可以看出,这里实现了异步并发

注意

这里有个注意点,在NSOperation中有个start方法,用这个方法并不能实现并发。在NSOperation中 isAsynchronous (iOS7之前是方法isConcurrent)返回值代表了operation相对了调用它的start方法的线程来说是否是异步执行的,而默认情况下,他返回的是NO,也就是说直接调用start方法,是不能够实现异步执行的,我们来试一下

@implementation ViewController
-(void)viewDidLoad{
    [super viewDidLoad];
//    self.myQueue = [[NSOperationQueue alloc] init];
    NSInvocationOperation *invocationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doSomething:) object:@"data"];
//    [self.myQueue addOperation:invocationOp];
    [invocationOp start];
    NSLog(@"%@",[NSThread currentThread]);
}
- (void)doSomething:(id)data{
    NSLog(@"%@-%@-%@",data,[NSThread mainThread],[NSThread currentThread]);
    sleep(3);
    NSLog(@"finish");
}
@end

看一下控制台的输出

2017-03-14 15:55:31.074 OperationQueues[11285:2454150] data-{number = 1, name = main}-{number = 1, name = main}
2017-03-14 15:55:34.146 OperationQueues[11285:2454150] finish
2017-03-14 15:55:34.147 OperationQueues[11285:2454150] {number = 1, name = main}

我们可以明显看到,doSomething是在主线程中执行,并且阻塞了线程,所以我们在进行operation并发的时候,都需要将operation添加到队列当中,才能够实现并发,因为当我们讲一个非并发的operation添加到队列中时,队列会自动为这个operation创建一个线程,来进行异步并发

BlockOperation

BlockOperation和Dispatch Queues比较类似,那么为什么有了Dispatch Queues还要使用BlockOperation呢?我们有时候会遇到这样的情况

  • 我们已经有了一个BlockOperation,不想再创建Dispatch Queues
  • 我们需要在operation之间设置一些依赖关系、进行一些KVO的观察,而这个Dispatch Queues是达不到的

同样来使用一下BlockOperation

class ViewController: UIViewController {
    let operationQueue = OperationQueue()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let blockOp = BlockOperation.init { 
            print("\(Thread.main)--\(Thread.current)")
            sleep(3)
            print("blockOp1 finish")
        }
        
        blockOp.addExecutionBlock {
            print("\(Thread.main)--\(Thread.current)")
            sleep(3)
            print("blockOp2 finish")
        }
        
        blockOp.addExecutionBlock {
            print("\(Thread.main)--\(Thread.current)")
            sleep(3)
            print("blockOp3 finish")
        }
        
        operationQueue.addOperation(blockOp)
        
    }
}

看一下控制台的输出

{number = 1, name = (null)}--{number = 3, name = (null)}
{number = 1, name = (null)}--{number = 4, name = (null)}
{number = 1, name = (null)}--{number = 5, name = (null)}
blockOp2 finish
blockOp3 finish
blockOp1 finish

每次运行,可能输出的结果都不一样,我们通过看线程地址,就能分辨出,每个block都是在一个单独的线程中执行

同样的,我们还是来试一下,使用start来进行启动queue

class ViewController: UIViewController {
//    let operationQueue = OperationQueue()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let blockOp = BlockOperation.init { 
            print("\(Thread.main)--1--\(Thread.current)")
            sleep(3)
            print("blockOp1 finish")
        }
        
        blockOp.addExecutionBlock {
            print("\(Thread.main)--2--\(Thread.current)")
            sleep(3)
            print("blockOp2 finish")
        }
        
        blockOp.addExecutionBlock {
            print("\(Thread.main)--3--\(Thread.current)")
            sleep(3)
            print("blockOp3 finish")
        }
        
//        operationQueue.addOperation(blockOp)
        blockOp.start()
        
    }
}

输出就截然不同

{number = 1, name = main}--1--{number = 1, name = main}
{number = 1, name = (null)}--2--{number = 3, name = (null)}
blockOp2 finish
{number = 1, name = (null)}--3--{number = 3, name = (null)}
blockOp1 finish
blockOp3 finish

完全没有规律的,每次输出都会不一样,但是这里有一个肯定是不会变的,不知道大家能不能猜到

就是首先加入到operation中的block都将会在当前调起的start的线程中执行,其他的block就不一定了

关于BlockOperation,只要其中的block大于1,就会触发多线程执行,但是这个多线程使我们完全不能控制的,都是系统调起,其中可能遇到阻塞,也可能不会遇到阻塞,这完全依赖于系统的分配

自定义Operation

我们自定义Operation对象,可以定义非并发的Operation也可以定义并发的Operation。什么意思呢?我们之前已经使用过了BlockOperation和NSInvocationOperation,两种都不能自身实现并发,我们这里可以自己实现自身并发的Operation,在调用start方法,即可以让他并发,下面我们来实现一下

非并发的Operation

对于非并发的Operation,很简单,我们只需要让它可以正常执行main方法中的任务就行,并且能够响应取消事件就行

关于响应取消事件,为了要响应取消事件,我们需要在执行时,不断检测isCancelled方法,看看返回是否是取消状态,在苹果的文档中有这样一句话

You should always check the value of this property before doing any work towards accomplishing the operation’s task, which typically means checking it at the beginning of your custom main() method. It is possible for an operation to be cancelled before it begins executing or at any time while it is executing. Therefore, checking the value at the beginning of your main() method (and periodically throughout that method) lets you exit as quickly as possible when an operation is cancelled.

上面的大致意思就是,在你执行main方法,或者main方法需要执行时(这个我的理解是在main方法中执行了一个循环),都应该检查这个operation 是否已经cancle掉了,换句话说,就是在main执行之前应该检查,在main中执行的循环,每次循环之前也应该检查

读到这里,我们应该就会了解到,其实operation的cancle不是立刻取消的,而是每次在检查isCancled的时候,才会来取消

class MyOperation: Operation {
    
    var message: String!
    var info: String!
    
    
    public init(message: String, messageInfo: String) {
        super.init()
        self.message = message//注意这里swift的写法,参数和变量相同,下面是不相同
        info = messageInfo
    }
    
    override func main() {
        
        if isCancelled {
            return
        }
        
        print("\(Thread.main)--\(Thread.current)")
        
        for var i in 1..<10 {
            
            if isCancelled {
                return
            }
            
            print("\(i)")
            
            sleep(2)
            
            i += 1
        }
    }
}

这样在main方法执行之前,和每次循环执行时,都会检测当前的OPeration是否已经cancle了,但是这个是一个非并发的Operation,我们使用start任然会卡掉发起start的线程,接下来我们来自定义一个可以并发的线程

并发的Operation

查看苹果文档,是最准确的,我们看看苹果文档上,是怎么说的我们自定义的一个并发的operation。

developer.apple.com/reference/f…
这是关于Operation的整个介绍,我们可以再下面找到具体的属性,方法,点进去查看,其中方法isFinished有这样一句话:

When implementing a concurrent operation object, you must override the implementation of this property so that you can return the finished state of your operation.

同样的,还有isExecuting

When implementing a concurrent operation object, you must override the implementation of this property so that you can return the execution state of your operation.

start

If you are implementing a concurrent operation, you must override this method and use it to initiate your operation.

大致我们能够了解到,这三个方法是必须要重写的,前两个方法是控制着当前operation的运行环境,是否正在运行,是否运行完成

start方法,我们就不说了,这个肯定要重写,因为要在这个里面重启线程来运行我们的operation任务

我们再来看一下main方法的说明

If you are implementing a concurrent operation, you are not required to override this method but may do so if you plan to call it from your custom start() method.

从官方的文档中,我们可以看出,我们不用必须重写main方法,但是如果我们想自己启动线程,异步执行operation的话,我们最好还是重写它,这是为什么呢?

其实我们看一下,start方法中,其实是为了配置当前的operation执行的环境(启动一个线程)

而我们真正执行的代码,虽然也可以方法start方法中,但是感觉逻辑有点混乱,如果我们放到main方法中,这样operation的结构更加清晰,明了

我们这里再加一个重写属性isReady,这个重写属性也是可选的,这里选择重写他,是为了展示一下重写这个方法的时候,需要设置KVO,因为在operation中,我们很多属性,都是可以KVO监测的,如果我们重写,都应该在属性改变时,通知外部,我们的属性改变了

好,我们来数一下,需要重写的方法或者属性的getter:

  • isFinished (必须)
  • isExecuting (必须)
  • start (必须)
  • main (非必须)
  • isReady (非必须)

首先我把整个类的代码都贴出来,再来解释

import Foundation
class MyAnotherOperation: Operation {
    
    var message: String?
    var info: String?
    
    enum State {
        case  ready, executing, finished
        var keyPath: String{
            switch self {
            case .ready:
                return "isReady"
            case .executing:
                return "isExecuting"
            case .finished:
                return "isFinished"
            }
        }
    }
    
    var state = State.ready {
        willSet{
            willChangeValue(forKey: newValue.keyPath)
            willChangeValue(forKey: state.keyPath)
        }
        didSet{
            didChangeValue(forKey: oldValue.keyPath)
            didChangeValue(forKey: state.keyPath)
        }
    }
    init(message: String, info: String) {
        super.init()
        self.message = message
        self.info = info
        
    }
    
    override func start() {
        if isCancelled {
            state = .finished
            return
        }
        
        state = .executing
        Thread.detachNewThreadSelector(#selector(Operation.main), toTarget: self, with: nil)
    }
    
    override func main() {
        print("\(Thread.main)--\(Thread.current)")
        for var i in 1..<10 {
            
            if isCancelled {
                return
            }
            
            print("\(i)")
            
            sleep(2)
            
            i += 1
        }
        sleep(3)
        
       state = .finished
    }
    
    // MARK: - override
    
    override var isReady: Bool{
        return super.isReady && state == .ready
    }
    
    override var isFinished: Bool{
        return state == .finished
    }
    
    override var isExecuting: Bool{
        return state == .executing
    }
    
    override var isAsynchronous: Bool{
            return true
    }
}

在这里,我是通过swift来实现的,相信读懂了的同学,用OC也能够简单实现,如果有什么疑问,再联系我好了

解释一下上面的代码:

因为,operation的属性isReady、isExecuting、isFinished都需要在改变时通知外部KVO通知,所以我们在将要改变他们的值和改变之后,都需要发出通知,如果我每个都去写,觉得麻烦,就定义了一个枚举State,代表当前的一个operation的状态

每次不管他们任何一个的状态需要改变时,我直接改变state的值,然后在state的willSet和didSet方法中来发出通知

这个时候就能感觉出,swift的强大之处了,枚举中能够定义属性,根据当前的枚举值,返回通知的key

isReady方法中

override var isReady: Bool{
    return super.isReady && state == .ready
}

不要忘记,一定要检查父类的ready属性

这样,我们每次,直接调用start方法,就是启了一个线程,在单独跑这个任务

配置operation之间的联系

我们之前在讲到operation和GCD的优缺点的时候,说到过,operation更加灵活,operation之间的灵活点之一就是operation之间可以添加依赖的,一个operation可以在另一个operation执行完成之后,再执行

注意

  • 在我们添加依赖的时候,需要在添加到队列之前,就将依赖设置好,如果在添加到队列之后设置,依赖就可能会失效,因为添加到队列,operation就随时可能被执行
  • 依赖是单向的a.addDependency(b),那么这个只会是a依赖于b,a在b执行完过后才会再次执行,所以,我们设置的时候不要设置成了一个循环依赖,这样operation都不会执行了
  • 设置operation不管是我们自定义的还是系统提供的都可以设置,没有区别
let blockOp1 = BlockOperation.init {
    print("\(Thread.main)--1--\(Thread.current)")
    sleep(5)
}
let blockOp2 = BlockOperation.init {
    print("\(Thread.main)--2--\(Thread.current)")
    sleep(5)
}
let blockOp3 = BlockOperation.init {
    print("\(Thread.main)--3--\(Thread.current)")
    sleep(5)
}
    
blockOp2.addDependency(blockOp1)
blockOp3.addDependency(blockOp2)
myQueue.addOperation(blockOp1)
myQueue.addOperation(blockOp2)
myQueue.addOperation(blockOp3)

上面的代码运行,就能看出blockOp1——> blockOp2——> blockOp3
这样的执行顺序

修改operation在队列中的优先级

这里需要注意的是,只有在同一队列中,修改operation的优先级才起作用,不然是不起作用的。

而且只有当一个operation的isReady为true的时候,这个优先级才起作用,举个例子

比如有两个operation,有一个高优先级的operation A 它的isReady为 false,还有一个低优先级的operation B 它的isReady为 true,那么这样的情况下,B仍然会先执行。

如果他们的isReady均为true 这个时候,才是A会首先执行

在Operation中有个枚举

public enum QueuePriority : Int {
    case veryLow
    case low
    case normal
    case high
    case veryHigh
}

其中决定了在队列中的优先级

修改operation执行任务的线程优先级

在iOS中,我们知道线程都是内核管理的,我们不能够操作线程的执行,但是我们可以修改线程的优先级,这样,内核就会优先分配线程给我们线程优先级高的线程执行

在Operation中有个属性

@available(iOS, introduced: 4.0, deprecated: 8.0)
open var threadPriority: Double

可以设置他的值从0.0到1.0 默认值为0.5

思考

这里,我们思考一个问题,如果这个operation是我们自定义的一个并发operation,那么它的线程是我们自己在start方法中启动的,因为我们没有调用super的start方法,所以这个线程的优先级就需要我们自己在start方法中,手动设置了。

设置 Completion Block

我们还是通过一段代码来分析

let blockOp1 = BlockOperation.init {
    print("\(Thread.main)--1--\(Thread.current)")
    sleep(5)
}
    
let blockOp2 = BlockOperation.init {
    print("\(Thread.main)--2--\(Thread.current)")
    sleep(5)
}
    
blockOp2.completionBlock = {
    if blockOp2.isCancelled {
        print("取消了");
        return
    }
    print("blockOp2执行完成")
}
    
let blockOp3 = BlockOperation.init {
    print("\(Thread.main)--3--\(Thread.current)")
    sleep(5)
}
    
blockOp2.addDependency(blockOp1)
blockOp3.addDependency(blockOp2)
myQueue.addOperation(blockOp1)
myQueue.addOperation(blockOp2)
myQueue.addOperation(blockOp3)
    
    
//在两秒后将blockOp2取消掉,看看会出现什么情况
let delayQueue = DispatchQueue(label: "com.ppsheep.delayqueue", qos: .userInitiated)
let additionalTime: DispatchTimeInterval = .seconds(2)
delayQueue.asyncAfter(deadline: .now()+additionalTime) { 
    print("开始取消blockOp2")
    blockOp2.cancel()
}

上面的代码,是我将三个operation分别添加到一个qoeue中,并且,我将blockOp2设置了completionBlock

看看输出

{number = 1, name = (null)}--1--{number = 3, name = (null)}
开始取消blockOp2
取消了
{number = 1, name = (null)}--3--{number = 4, name = (null)}

通过上面的输出,我们可以总结几点

  • 不管是operation是否取消,他的completionBlock都会执行,所以我们在执行completionBlock的时候需要检查一下他是否已经被取消了
  • 三个operation互相之间设置了依赖,当blockOp2被取消时,blockOp3并没有影响执行,所以这里也可以看出,当依赖之间的operation取消时,并不会影响其他operation的执行

注意

这里还有一点需要注意,一般来说,我们的completionBlock回调线程都是和发起start或者queue的同一线程,所以如果是需要更新UI,最好在completionBlock中,使用GCD,扔到主线程来执行

关于Operation Queues的我们需要了解的知识,大概已经讲完了,在我们日常的使用中,上面讲到的应该已经够用了

参考:
www.objccn.io/issue-2-3/

欢迎大家关注我的公众号,我会定期分享一些我在项目中遇到问题的解决办法和一些iOS实用的技巧,现阶段主要是整理出一些基础的知识记录下来

上边是公众号,下边是我个人微信




文章也会同步更新到我的博客:
ppsheep.com