I have a number of NSOperations
which create some data asynchronously. I want to collect all of the results into one array. Because I'm accessing the array on multiple different threads, I've put locking around the array.
The NSOperationQueue
is appending the data to the array but the results seem to miss some of the data objects. The results seem to change each time I run it.
I've created a simplified example project that recreates the issue. The code is in Swift but I don't think this is Swift-specific.
import UIKit
class ViewController: UIViewController {
let queue = NSOperationQueue()
var bucket = [String]()
override func viewDidLoad() {
super.viewDidLoad()
queue.addObserver(self, forKeyPath: "operations", options: NSKeyValueObservingOptions.New, context: nil)
for _ in 0..<10 {
queue.addOperation(NSBlockOperation {
// Let's pretend that creating the "fish" string is actually potentially
// expensive and that's why we're doing it in an NSOperation.
let fish = "fish"
objc_sync_enter(self.bucket)
self.bucket.append(fish)
let fishCount = self.bucket.count
print("Bucket contains \(fishCount) fish" + ((fishCount != 1) ? "es" : ""))
objc_sync_exit(self.bucket)
})
}
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if let keyPath = keyPath {
if let object = object as? NSOperationQueue {
if object == queue && keyPath == "operations" {
if queue.operationCount == 0 {
objc_sync_enter(self.bucket)
let fishCount = bucket.count
print("Bucket finally contains \(fishCount) fish" + ((fishCount != 1) ? "es" : ""))
objc_sync_exit(self.bucket)
}
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
}
}
}
The results vary but are often something like this:
Bucket contains 1 fish
Bucket contains 1 fish
Bucket contains 1 fish
Bucket contains 1 fish
Bucket contains 2 fishes
Bucket contains 1 fish
Bucket contains 1 fish
Bucket contains 3 fishes
Also, sometimes the code crashed with an EXC_BAD_ACCESS
on the line self.bucket.append(fish)
In addition, the line print("Bucket finally contains \(fishCount) fish" + ((fishCount != 1) ? "es" : ""))
in observeValueForKeyPath
never gets called. I'm not sure if this is a separate issue or not.
You should look at subclassing NSOperation, since it is an abstract class. See this Stackoverflow question for subclassing. With that in mind I would suggest that you have an identifier property on each operation instance so that you can keep track of your operations, that way you can tell when all of your operations have finished. You might also consider pulling this code out of your view controller class and creating a class to handle your fish Plus it will help you with encapsulation further down the road when say you are no longer interested in fish but cats :)
The Concurrency Programming Guide is really good at explaining the basics of asynchronous application design.
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 NSBlockOperation) 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.