swiftnsoperationqueuensoperation

NSOperation Queue waitUntilAllOperationsAreFinished causes NSOperation instance deinit late


Our app currently using NSOperation (Operation in Swift) to manage serials of network request and data parsing. Some code are required to be executed after like all 5 operations in a queue are finished, which typically implemented with GCD group.

DispatchQueue.global().async {
    (0...5).forEach(){
        self.queue.addOperation(CustomOperation(value: $0))
    }
    self.queue.waitUntilAllOperationsAreFinished()
    print("All Tasks Done")
}

The issue is NSOperation instance not deinit until all 5 operations done, which is causing memory release late than it supposed to. If queue.waitUntilAllOperationsAreFinished is removed, the instance will be deinit immediately.

We've added autorelease pool to avoid it. But is it possible to make NSOperation instance deinit immediately when use waitUntilAllOperationsAreFinished?

prints with waitUntilAllOperationsAreFinished

Begin Task 5
Begin Task 4
Begin Task 3
Begin Task 2
Begin Task 1
Begin Task 0
Finish Task 0
Finish Task 1
Finish Task 2
Finish Task 3
Finish Task 4
Finish Task 5
deinit 0
deinit 1
deinit 2
deinit 3
deinit 4
deinit 5
All Tasks Done

prints without waitUntilAllOperationsAreFinished

All Tasks Done
Begin Task 0
Begin Task 1
Begin Task 4
Begin Task 3
Begin Task 5
Finish Task 0
Begin Task 2
deinit 0
Finish Task 1
deinit 1
Finish Task 2
deinit 2
Finish Task 3
deinit 3
Finish Task 4
deinit 4
Finish Task 5
deinit 5

The custom operation.

class CustomOperation: Operation {
    
    public enum State {
        case ready
        case running
        case finished
    }

    private var state: State = .ready
    
    override var isAsynchronous: Bool { return true }
    
    override open var isExecuting: Bool { state == .running }

    override open var isFinished: Bool { state == .finished }

    var value: Int = 0
    
    init(value: Int) {
        super.init()
        
        self.value = value
    }
    
    override func main() {
        print("Begin Task \(value)")
        DispatchQueue.global().asyncAfter(deadline: .now()+DispatchTimeInterval.seconds(value)) {
            print("Finish Task \(self.value)")
            self.finish()
        }
    }
    
    func finish() {
        willChangeValue(forKey: "isExecuting")
        willChangeValue(forKey: "isFinished")
        state = .finished
        didChangeValue(forKey: "isFinished")
        didChangeValue(forKey: "isExecuting")
    }
    
    deinit {
        print("deinit")
    }
}

Solution

  • Didn't know about that behavior but I don't think that you can do something here.
    If you are concerned about operation order you can set the queue to have a maxConcurrentOperationCount to 1 so that you can keep order.
    If you are concerned about memory and you have some huge data you can get rid of that in the finish() method or use a completionBlock to pass it around.
    There also the option to use KVO on the OperationQueue properties, most of its properties are KVO and KVC compliant and you can set observation on some of them to trigger a callback.
    If you are deploying target >=13 you can use Combine as already written by vadian.