0

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.

Nikunj
  • 655
  • 3
  • 13
  • 25
Josh Paradroid
  • 1,172
  • 18
  • 45

1 Answers1

1

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.

Community
  • 1
  • 1
Peter Hornsby
  • 4,208
  • 1
  • 25
  • 44
  • I'm using NSBlockOperations. Is this not correct? Also, this is just an example of the issue I'm facing. The project that this relates to is organised in a much better way. – Josh Paradroid Dec 18 '15 at 13:41
  • Yes you can use NSBlockOperation, but what if you need to cancel an operation? You have no way of doing it. How do you keep track of your operations. If you subclass NSOperation, I would still use blocks. It is a design choice and using NSBlockOperation requires you to do more of the foot work. Either way I would move this code out of you view controller. Ask yourself how do you intend to manage these operations? – Peter Hornsby Dec 18 '15 at 13:48
  • Let's assume, for the moment, that I never need to cancel these operations and that I don't need to know when all operations are finished. The issue of putting 10 fish into the bucket is still not happening correctly. This is what I'd like to know how to do asynchronously and correctly. – Josh Paradroid Dec 18 '15 at 13:54
  • dispatch_async(dispatch_get_main_queue(), ^{ // do work here }); – Peter Hornsby Dec 18 '15 at 13:58
  • Excuse me, try using the dispatch_async method on the main queue where you currently use objc_sync_enter. – Peter Hornsby Dec 18 '15 at 14:00
  • see this question for monitoring the queue http://stackoverflow.com/questions/1049001/get-notification-when-nsoperationqueue-finishes-all-tasks – Peter Hornsby Dec 18 '15 at 14:04
  • Dispatching the appending to the array on the main thread works perfectly. Many thanks. As for the notification when the operation completes, that now fires. I used the ticked answer in the URL you mentioned as the basis for mine. I also have to dispatch the printing to the main queue too, otherwise it occurred before the fish have actually been added to the bucket! – Josh Paradroid Dec 18 '15 at 14:12