Asynchronous methods in NSOperation
Asked Answered
S

4

35

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

Skyline answered 20/10, 2009 at 17:33 Comment(3)
If FBConnect is inherently asynchronous, is there really any need to use an NSOperation at all?Asch
Well yes as it's one of many queued up tasks and there is a lot of processing that occurs after the data is downloaded.Skyline
Why not create the NSOperation for that processing once the asynchronous download is complete, then?Asch
C
27

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
Canella answered 5/11, 2015 at 21:58 Comment(6)
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 *)contextMcmath
Argh. No, just no. Do not define properties that start with _. Redefine the existing properties. Call super if you need to.Duran
executing and finished exist in NSOperation, but are readonly so they cannot be modified. How could this be cleaner?Canella
@JasonMoore How do you redefine them? You just do. There's no complexity to this, and there's no "cannot" to it.Duran
I'm afraid "There's no complexity to this" is a BFL, having tried to do that for several hours, with 35 years of KVO and cocoa experience. Last, you MUST "@synthesize" for the underlying storage, but you also MUST re-implement all the accessors and avoid all synthesized and auto-synthesized accessors.Laktasic
See my answer below (from 2019) for the answer to "cannot" redefine. You also need not synthesize to get storage, though I certainly would.Duran
C
6

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.

Cayman answered 12/7, 2011 at 22:35 Comment(1)
As of iOS 7.0, isAsynchronous should be used instead of isConcurrent.Canella
D
2

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
Duran answered 22/1, 2019 at 21:16 Comment(2)
I'm almost bursting into tears of thankfulness, even before trying this out. This is exactly what I was looking for. I got the idea alright from the docs, but overriding those readonly base-class KVO compliant properties brought a hell of compiler and other issues. One thing you did NOT mention and I think it is important - When and I what circumstances should you call [super cancel] or [super anything] ? how do you employ base-class machinery and implementation?Laktasic
Oh! If you override a method, unless otherwise documented you should call its super.Duran
P
0

How about this?

//
//  Operation.m

#import "Operation.h"

@interface Operation() 

@property (nonatomic, strong) dispatch_semaphore_t semaphore;

@end

@implementation Operation 

- (void)main {
    [self doWorkWithCompletion:^{
        dispatch_semaphore_signal(self.semaphore);
    }];    
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
}

....

@end
Pentlandite answered 1/1, 2021 at 22:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.