Asynchronous methods in NSOperation

15,865

Solution 1

Below is a full example. In your subclass, after your async method completes, call [self completeOperation] to transition to the finished state.

@interface AsynchronousOperation()
// 'executing' and 'finished' exist in NSOperation, but are readonly
@property (atomic, assign) BOOL _executing;
@property (atomic, assign) BOOL _finished;
@end

@implementation AsynchronousOperation

- (void) start;
{
    if ([self isCancelled])
    {
        // Move the operation to the finished state if it is canceled.
        [self willChangeValueForKey:@"isFinished"];
        self._finished = YES;
        [self didChangeValueForKey:@"isFinished"];
        return;
    }

    // If the operation is not canceled, begin executing the task.
    [self willChangeValueForKey:@"isExecuting"];
    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
    self._executing = YES;
    [self didChangeValueForKey:@"isExecuting"];

}

- (void) main;
{
    if ([self isCancelled]) {
        return;
    }

}

- (BOOL) isAsynchronous;
{
    return YES;
}

- (BOOL)isExecuting {
    return self._executing;
}

- (BOOL)isFinished {
    return self._finished;
}

- (void)completeOperation {
    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];

    self._executing = NO;
    self._finished = YES;

    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

@end

Solution 2

put your FBConnect calls in 'start', not 'main', and manage the 'isFinished' 'isExecuting' properties. (and return YES for 'isConcurrent')

For more details, see Apple's documentation on writing concurrent NSOperations.

Solution 3

Please understand this if nothing else: There's nothing magic about NSOperation's behaviour. NSOperationQueue just uses Key Value Observation to monitor operations. The only reason why this isn't painfully easy is that the keys used aren't the same as what Objective-C 2.0 conventions say they should be, so the standard synthesized setters won't work.

The result is that when you define your NSOperation subclass, you need to provide asynchronous, executing and finished. And those last two need a bit of help on your part to work properly.

Sound complicated? It's not, it's just details. Each step along the way is simple and makes sense, but it won't actually work until you get all of them right.

First, the header:

//
//  MyOperation.h

#import <Foundation/Foundation.h>

@interface MyOperation : NSOperation

@property(readonly, getter=isAsynchronous) BOOL asynchronous;
@property(readonly, getter=isExecuting) BOOL executing;
@property(readonly, getter=isFinished) BOOL finished;

@end

You could, of course, define executing and finished as readwrite here so you don't need to redefine them as readwrite in the implementation. But I like to know only my operations can change their state.

Now the implementation. There's a few steps here:

  • redefine finished and executing properties as read/write.
  • fully provide an implementation of executing and finished that manually provides the correct KVO messaging (so isExecuting, setExecuting:, isFinished and setFinished:).
  • provide storage for executing and finished ivars using @synthesize.
  • provide the implementation of asynchronous

(Note that this code will probably scroll a bit.)

//
//  MyOperation.m

#import "MyOperation.h"

@interface MyOperation()
@property(readwrite) BOOL executing;
@property(readwrite) BOOL finished;
@end

@implementation MyOperation

// Provide your own start.

- (void)start {
    if (self.cancelled) {
        self.finished = YES;
        return;
    }
    NSLog(@"Starting %@", self);
    self.executing = YES;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        NSLog(@"Finished %@", self);
        self.executing = NO;
        self.finished = YES;
    });
}

// The rest of this is boilerplate.

- (BOOL)isAsynchronous {
    return YES;
}

@synthesize executing = _executing;

- (BOOL)isExecuting {
    @synchronized(self) {
        return _executing;
    }
}

- (void)setExecuting:(BOOL)executing {
    @synchronized(self) {
        if (executing != _executing) {
            [self willChangeValueForKey:@"isExecuting"];
            _executing = executing;
            [self didChangeValueForKey:@"isExecuting"];
        }
    }
}

@synthesize finished = _finished;

- (BOOL)isFinished {
    @synchronized(self) {
        return _finished;
    }
}

- (void)setFinished:(BOOL)finished {
    @synchronized(self) {
        if (finished != _finished) {
            [self willChangeValueForKey:@"isFinished"];
            _finished = finished;
            [self didChangeValueForKey:@"isFinished"];
        }
    }
}


@end

It's not really necessary to check (for example) executing != _executing in the setter. The correct behaviour is provided automatically by calling willChangeValueForKey, blindly changing the value, then calling didChangeValueForKey. But the condition means you can put a breakpoint down on the assignment and only stop when the value is changed, and I've found that incredibly useful for debugging my operations in practice.

I've also seen this implemented by providing a custom state on top of the executing and finished properties. This works perfectly well, of course, and is in some ways better… but it also requires more knowledge of KVO than this example, and this is already enough.

Finally, note that I have not added support for cancel once the operation starts. To do that, you'd have to override cancel (or maybe, more correctly, observe the value of isCancelled) and handle it. That would complicate my simple start example a lot.

I ran this code in a command line console app by adding 15 operations to a queue with a maxConcurrentOperationCount of 5 then waiting on the queue to finish using waitUntilAllOperationsAreFinished (this is why I used a background queue for dispatch_after in my start). This is the output:

2019-01-22 13:29:32.897893-0800 test[86762:4812871] Starting <MyOperation: 0x10058d2d0>
2019-01-22 13:29:32.897893-0800 test[86762:4812872] Starting <MyOperation: 0x10058d710>
2019-01-22 13:29:32.897903-0800 test[86762:4812873] Starting <MyOperation: 0x100589930>
2019-01-22 13:29:32.898161-0800 test[86762:4812871] Starting <MyOperation: 0x10058edc0>
2019-01-22 13:29:32.898166-0800 test[86762:4812873] Starting <MyOperation: 0x10058ed50>
2019-01-22 13:29:37.898487-0800 test[86762:4812872] Finished <MyOperation: 0x100589930>
2019-01-22 13:29:37.898489-0800 test[86762:4812870] Finished <MyOperation: 0x10058ed50>
2019-01-22 13:29:37.898548-0800 test[86762:4812874] Finished <MyOperation: 0x10058edc0>
2019-01-22 13:29:37.898797-0800 test[86762:4812870] Starting <MyOperation: 0x100590000>
2019-01-22 13:29:37.899160-0800 test[86762:4812870] Finished <MyOperation: 0x10058d710>
2019-01-22 13:29:37.899651-0800 test[86762:4812870] Starting <MyOperation: 0x1005901a0>
2019-01-22 13:29:37.899933-0800 test[86762:4812874] Starting <MyOperation: 0x100590340>
2019-01-22 13:29:37.900133-0800 test[86762:4812871] Finished <MyOperation: 0x10058d2d0>
2019-01-22 13:29:37.900504-0800 test[86762:4812871] Starting <MyOperation: 0x100590680>
2019-01-22 13:29:37.900583-0800 test[86762:4812874] Starting <MyOperation: 0x1005904e0>
2019-01-22 13:29:42.899325-0800 test[86762:4812871] Finished <MyOperation: 0x100590000>
2019-01-22 13:29:42.899541-0800 test[86762:4812874] Starting <MyOperation: 0x100590820>
2019-01-22 13:29:43.393291-0800 test[86762:4812871] Finished <MyOperation: 0x1005901a0>
2019-01-22 13:29:43.393298-0800 test[86762:4812874] Finished <MyOperation: 0x100590340>
2019-01-22 13:29:43.394531-0800 test[86762:4812874] Finished <MyOperation: 0x1005904e0>
2019-01-22 13:29:43.395380-0800 test[86762:4812874] Finished <MyOperation: 0x100590680>
2019-01-22 13:29:43.396359-0800 test[86762:4812874] Starting <MyOperation: 0x1005909c0>
2019-01-22 13:29:43.397440-0800 test[86762:4812872] Starting <MyOperation: 0x100590b60>
2019-01-22 13:29:43.397891-0800 test[86762:4812874] Starting <MyOperation: 0x100590d00>
2019-01-22 13:29:43.399711-0800 test[86762:4812872] Starting <MyOperation: 0x100590ea0>
2019-01-22 13:29:47.900058-0800 test[86762:4812984] Finished <MyOperation: 0x100590820>
2019-01-22 13:29:48.892953-0800 test[86762:4812872] Finished <MyOperation: 0x100590d00>
2019-01-22 13:29:48.892970-0800 test[86762:4812871] Finished <MyOperation: 0x100590b60>
2019-01-22 13:29:48.893019-0800 test[86762:4813163] Finished <MyOperation: 0x100590ea0>
2019-01-22 13:29:48.893562-0800 test[86762:4812984] Finished <MyOperation: 0x1005909c0>
Program ended with exit code: 0
Share:
15,865
Michael Waterfall
Author by

Michael Waterfall

Updated on June 05, 2022

Comments

  • Michael Waterfall
    Michael Waterfall almost 2 years

    I'm fetching some data from Facebook Connect (using the FBConnect Objective-C 2.0 framework) and I'm doing all that in an NSOperation. It is in an NSOperation because I have several other operations that run as well and this is one of them.

    The problem is that all the FBConnect calls are asynchronous. Because of this, the main method of the NSOperation quickly finishes and the operation is marked as completed.

    Is there some way to overcome this? It would appear there are no synchronous options in FBConnect!

    Many thanks,

    Mike

  • Jason Moore
    Jason Moore over 8 years
    As of iOS 7.0, isAsynchronous should be used instead of isConcurrent.
  • eric
    eric about 8 years
    That said... for other uses, like maybe a class that subclasses NSObject, and handles the NSOperationQueue for you... make sure you impl: -(void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  • Steven Fisher
    Steven Fisher over 7 years
    Argh. No, just no. Do not define properties that start with _. Redefine the existing properties. Call super if you need to.
  • Jason Moore
    Jason Moore over 7 years
    executing and finished exist in NSOperation, but are readonly so they cannot be modified. How could this be cleaner?
  • Steven Fisher
    Steven Fisher over 5 years
    @JasonMoore How do you redefine them? You just do. There's no complexity to this, and there's no "cannot" to it.