并发编程 - NSOperation&NSOperationQueue(多线程)-LMLPHP

引言

在上篇博客中我们首先介绍了GCD的多线程方案,NSOperation和NSOperationQueue是Apple为我们提供的另一个并发编程框架的高级抽象,用于简化和管理复杂的多线程任务。事实上它基于GCD的高层封装,提供了更强大的功能和更灵活的控制。

尽管GCD非常强大,但在某些场景下,开发者需要对任务的管理有更多的控制,比如任务的依赖关系,取消任务,任务完成后的处理等,这时候NSOperation和NSOperationQueue的优势就凸显出来了,他可以帮助开发者更方便地管理复杂的并发任务。

概念

NSOperation(操作)

NSOperation是一个抽象类,代表一个可以被加入到队列中执行的操作。每个NSOperation实例都代表一个独立的任务,这些任务可以被添加到NSOperationQueue中,并按照指定的顺序和依赖关系来执行。

NSOperation提供了对任务的基本控制功能,例如开始、暂停、取消,以及查询任务的状态。它还可以管理任务的执行顺序,通过设置任务之间的依赖关系,确保某个任务在另一个任务完成之后再执行。

此外,NSOperation可以通过子类化来创建自定义的操作,或者直接使用其子类BlockOperation来快速封装任务。

NSOperationQueue(操作队列)

NSOperationQueue是一个用于调度和管理NSOperation的队列,负责控制NSOperation的执行顺序、并发性以及任务间的依赖关系。

NSOperationQueue可以同时执行多个操作,并通过设置最大并发操作数来控制同时执行的任务数量。根据操作之间的依赖关系,NSOperationQueue能够自动调度任务的执行顺序。

此外,NSOperationQueue允许在运行时取消或者暂停队列中的操作,并控制它们的执行。

NSOperationQueue还提供了一个特殊的队列,成为主队列(mainQueue),它用于在主线程上执行操作,非常适合需要更新UI的任务。

使用

直接执行任务

对于NSOperation我们需要使用它的子类比如BlockOperation,使用它我们可以通过代码块(block)来创建任务,并且可以直接执行任务。

    func testOperation() {
        let operation = BlockOperation {
            for i in 0...5 {
                print("BlockOperation:\(i)")
            }
        }
        operation.start()
    }

通过队列管理操作

不过通常来讲,我们会把操作和操作队列组合在一起使用,通过操作队列来自动管理队列中操作的执行,而不是手动调用start()方法。

    // MARK: - 创建OperationQueue
    private func testOperationQueue() {
        let operationQueue = OperationQueue()
        let operation1 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation:\(i)")
            }
        }
        let operation2 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation:\(i)")
            }
        }
        operationQueue.addOperation(operation1)
        operationQueue.addOperation(operation2)
    }

设置优先级

NSOperation提供了一种简单的方式来设置操作的优先级(queuePriority)。通过设置操作的优先级,我们可以在队列中多个操作等待执行时,控制哪些操作应该优先执行。优先级从.veryLow到.veryHigh共有5个级别。

    // MARK: - 设置优先级
    private func testPriority() {
        let operationQueue = OperationQueue()
        let operation1 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation1:\(i)")
            }
        }
        let operation2 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation2:\(i)")
            }
        }
        let operation3 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation3:\(i)")
            }
        }
        operation1.queuePriority = .normal
        operation2.queuePriority = .high
        operation3.queuePriority = .low
        operationQueue.addOperations([operation1, operation2, operation3], waitUntilFinished: false)
    }

需要注意的是:

优先级只在所有操作都处于准备状态时才有效,不能覆盖操作之间的依赖关系。

比如,当多个图片处理人物同时被添加到队列中,我们可以设置其中一个任务为高优先级,确保它比其他任务更早执行。

设置依赖关系

NSOperation提供了非常简单的方式来设置依赖关系,这在需要严格按照先后顺序执行的场景中非常有用。通过将一个操作设置为另一个操作的依赖项,我们可以确保操作按照预期的顺序执行,从而轻松管理复杂的任务流程。例如,在处理网络请求、文件操作或任务链条时,依赖关系能够帮助我们在不引入大量额外代码的情况下,轻松控制任务的执行顺序。

    // MARK: - 设置依赖
    private func testDependency() {
        let operationQueue = OperationQueue()
        let operation1 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation1:\(i)")
            }
        }
        let operation2 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation2:\(i)")
            }
        }
        let operation3 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation3:\(i)")
            }
        }
        operation2.addDependency(operation1)
        operation3.addDependency(operation2)
        operationQueue.addOperations([operation1, operation2, operation3], waitUntilFinished: false)
    }

设置最大并发数

NSOperationQueue是一个并发队列,默认的最大并发数为OperationQueue.defaultMaxConcurrentOperationCount,但实际上这并不是一个固定的值,它是一个动态计算的值,取决于系统资源,硬件能力和其他因素。

但我们可以通过maxConcurrentOperationCount来修改队列的最大并发数。

    //MARK: - 设置最大并发数
    private func testMaxConcurrentOperationCount() {
        let operationQueue = OperationQueue()
        operationQueue.maxConcurrentOperationCount = 2
        let operation1 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation1:\(i)")
                sleep(1)
            }
        }
        let operation2 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation2:\(i)")
                sleep(1)
            }
        }
        let operation3 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation3:\(i)")
                sleep(1)
            }
        }
        operationQueue.addOperations([operation1, operation2, operation3], waitUntilFinished: false)
    }

阻塞当前线程

队列还为我们提供了一个确保队列中所有的操作都已经执行完毕后再继续执行后面代码的方法,这在实际开发中有着广泛的应用场景,例如,在处理多个媒体资源的上传后统一发布,或者在多个数据资源加载完成后统一渲染时,waitUntilAllOperationsAreFinished方法可以确保所有必要的操作都已经完成。

    //MARK: - 阻塞线程
    private func testWaitUntilFinished() {
        let operationQueue = OperationQueue()
        let operation1 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation1:\(i)")
            }
        }
        let operation2 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation2:\(i)")
            }
        }
        let operation3 = BlockOperation {
            for i in 0...5 {
                print("BlockOperation3:\(i)")
            }
        }
        operationQueue.addOperations([operation1, operation2, operation3], waitUntilFinished: true)
        print("finished")
    }

但是我们在使用阻塞线程的方法时,需要注意避免阻塞主线程,这可能会导致应用的UI停止响应。

另外还需要注意避免死锁,如果NSOperationQueue中的某个操作依赖于当前线程的结果,那么使用waitUntilAllOperationAreFinished可能会导致死锁,因此需要确保操作之间没有相互依赖。

取消操作

在实际开发中,有时候我们需要取消一个已经添加到队列中的操作,例如,当用户中途取消了下载任务,或者某些操作在特定条件下变得不再必要时,取消操作可以帮助我们更好地管理资源和提高应用性能。

NSOperation提供了一个简单的方法cancel(),用于取消操作。执行完该方法后,该操作的isCancelled属性为被设置为true,我们可以在操作的执行过程中检查这个属性,以便提前结束任务。

let operation = BlockOperation {
    if !operation.isCancelled {
        print("Operation started")
        // 执行任务...
    }
    if operation.isCancelled {
        print("Operation was cancelled")
        return
    }
    print("Operation completed")
}

operation.cancel()

有两点需要注意的:

  1. 取消操作不会立即终止操作的执行。操作本身需要定期检查isCancelled属性,并在适当的时候结束任务。
  2. 对于自定义的NSOperation子类,需要在适当的位置添加对isCancelled的检查,以支持取消操作。

如果需要取消NSOperation中的所有操作,可以使用cancelAllOperations()方法,这将取消队列种的所有操作,包括那些尚未开始的操作和正在执行的操作。

let queue = OperationQueue()

let operation1 = BlockOperation {
    print("Operation 1 started")
    Thread.sleep(forTimeInterval: 2)
    print("Operation 1 completed")
}

let operation2 = BlockOperation {
    print("Operation 2 started")
    Thread.sleep(forTimeInterval: 2)
    print("Operation 2 completed")
}

queue.addOperations([operation1, operation2], waitUntilFinished: false)

// 取消所有操作
queue.cancelAllOperations()

结语

通过本文,我们探讨了NSOperation和NSOperationQueue的基本使用方法,操作优先级,操作依赖关系,最大并发数,阻塞线程以及取消操作。这些功能使得我们在iOS开发中能够灵活地管理并发任务和操作队列,从而提高应用的性能和响应性能。掌握这些技术,可以帮助我们更好地处理复杂的任务流程、优化资源使用、增强用户体验。

希望本文对你理解和应用NSOperation和NSOperationQueue提供了有价值的帮助。如果你有任何疑问或需要进一步探讨,请随时留言交流。

09-10 14:36