Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSOperation wait until asynchronous block executes

i need to put asynchronous operations into an operation queue, however, they need to execute on after the other

self.operationQueue = [NSOperationQueue new];
self.operationQueue.maxConcurrentOperationCount = 1;

[self.operationQueue addOperationWithBlock:^{

    // this is asynchronous
    [peripheral1 connectWithCompletion:^(NSError *error) {

    }];

}];

[self.operationQueue addOperationWithBlock:^{

    // this is asynchronous
    [peripheral2 connectWithCompletion:^(NSError *error) {

    }];

}];

the problem is, since peripheralN connectWithCompletion is asynchronous, the operation in queue is ended and the next is executed, i would need however simulate, that peripheralN connectWithCompletion is synchronous and wait with the end of operation, till the asynchronous block executes

so i would need a behaviour like this, only using the operation queue

    [peripheral1 connectWithCompletion:^(NSError *error) {

            [peripheral2 connectWithCompletion:^(NSError *error) {

            }];

    }];
like image 565
Peter Lapisu Avatar asked Nov 12 '14 14:11

Peter Lapisu


People also ask

What method in an NSOperation subclass contains the work the operation will perform?

You typically execute operations by adding them to an operation queue (an instance of the NSOperationQueue class). An operation queue executes its operations either directly, by running them on secondary threads, or indirectly using the libdispatch library (also known as Grand Central Dispatch).

Is NSOperation thread-safe?

NSOperation represents a single unit of work. It's an abstract class that offers a useful, thread-safe structure for modeling state, priority, dependencies, and management.

What is asynchronous operation in Swift?

Asynchronous operations allow executing long-running tasks without having to block the calling thread until the execution completes. It's a great way to create separation of concern, especially in combination with creating dependencies in-between operations.


3 Answers

NSBlockOperation can't handle asynchronous operations, but it's not all that hard to create a subclass of NSOperation that can...

Basically, you need to create an NSOperation that takes a block that takes another block as a completion handler. The block could be defined like this:

typedef void(^AsyncBlock)(dispatch_block_t completionHandler);

Then, in your NSOperation subclass's start method, you need to call your AsyncBlock passing it a dispatch_block_t that will be called when it's done executing. You also need to be sure to stay KVO compliant with NSOperation's isFinished and isExecuting properties (at minimum) (see KVO-Compliant Properties in the NSOperation docs); this is what allows the NSOperationQueue to wait until your asynchronous operation is complete.

Something like this:

- (void)start {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = YES;
    [self didChangeValueForKey:@"isExecuting"];

    self.block(^{
        [self willChangeValueForKey:@"isExecuting"];
        _executing = NO;
        [self didChangeValueForKey:@"isExecuting"];
        [self willChangeValueForKey:@"isFinished"];
        _finished = YES;
        [self didChangeValueForKey:@"isFinished"];
    });
}

Note that _executing and _finished will need to be defined in your subclass somewhere, and you'll need to override isExecuting and isFinished properties to return the correct values.

If you put all that together, along with an initializer that takes your AsyncBlock, then you can add your operations to the queue like this:

[self.operationQueue addOperationWithBlock:^(dispatch_block_t completionHandler) {
    [peripheral1 connectWithCompletion:^(NSError *error) {
        // call completionHandler when the operation is done
        completionHandler();
    }];
}];

[self.operationQueue addOperationWithBlock:^(dispatch_block_t completionHandler) {
    [peripheral2 connectWithCompletion:^(NSError *error) {
        // call completionHandler when the operation is done
        completionHandler();
    }];
}];

I put together a gist of a simple version of this here: AsyncOperationBlock. It's only a minimum implementation, but it should work (it would be nice if isCancelled where also implemented, for example).

Copied here for completeness:

AsyncBlockOperation.h:

#import <Foundation/Foundation.h>

typedef void(^AsyncBlock)(dispatch_block_t completionHandler);

@interface AsyncBlockOperation : NSOperation

@property (nonatomic, readonly, copy) AsyncBlock block;

+ (instancetype)asyncBlockOperationWithBlock:(AsyncBlock)block;

- (instancetype)initWithAsyncBlock:(AsyncBlock)block;

@end


@interface NSOperationQueue (AsyncBlockOperation)

- (void)addAsyncOperationWithBlock:(AsyncBlock)block;

@end

AsyncBlockOperation.m:

#import "AsyncBlockOperation.h"

@interface AsyncBlockOperation () {
    BOOL _finished;
    BOOL _executing;
}

@property (nonatomic, copy) AsyncBlock block;

@end


@implementation AsyncBlockOperation

+ (instancetype)asyncBlockOperationWithBlock:(AsyncBlock)block {
    return [[AsyncBlockOperation alloc] initWithAsyncBlock:block];
}

- (instancetype)initWithAsyncBlock:(AsyncBlock)block {
    if (self = [super init]) {
        self.block = block;
    }
    return self;
}

- (void)start {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = YES;
    [self didChangeValueForKey:@"isExecuting"];

    self.block(^{
        [self willChangeValueForKey:@"isExecuting"];
        _executing = NO;
        [self didChangeValueForKey:@"isExecuting"];
        [self willChangeValueForKey:@"isFinished"];
        _finished = YES;
        [self didChangeValueForKey:@"isFinished"];
    });
}

- (BOOL)isFinished {
    return _finished;
}

- (BOOL)isExecuting {
    return _executing;
}

- (BOOL)isAsynchronous {
    return YES;
}

@end

@implementation NSOperationQueue (AsyncBlockOperation)

- (void)addAsyncOperationWithBlock:(AsyncBlock)block {
    [self addOperation:[AsyncBlockOperation asyncBlockOperationWithBlock:block]];
}

@end
like image 183
Mike S Avatar answered Nov 02 '22 20:11

Mike S


What I did was playing with [myQueue setSuspended:YES] and [myQueue setSuspended:NO] before and after respectively.

For example:

[myQueue addOperationWithBlock:^{
    [myQueue setSuspended:YES];
    [someBackendService doSomeAsyncJobWithCompletionBlock:^{
        callback(YES, nil);
        [myQueue setSuspended:NO];
    });
}];

The achieved effect is that the queue is suspended before the async task, therefore even though the operation block is returned, it only starts the next operation (subject to maxConcurrentOperationCount of course) when the async task's completion block is called.

like image 21
mllm Avatar answered Nov 02 '22 18:11

mllm


Based on the solution of @mllm by using setSuspended: I was finally able to run my FOR loop of asynchronous HTTP GET requests in a sequential order. The server did not allow concurrent connections and returned an error when it was flooded with requests.

The below solution resolves this, since the next NSOperation is only started upon completion of the previous operation:

@property (nonatomic) NSOperationQueue *myQueue;

- (void)requestVersions {
    // Create NSOperationQueue for serial retrieval:
    _myQueue = [[NSOperationQueue alloc] init];
    _myQueue.maxConcurrentOperationCount = 1;
    
    // Parse array:
    for (NSObject *object in _array) {
        // Add block operation:
        [_myQueue addOperationWithBlock:^{
            // Suspend next execution until request is completed:
            [_myQueue setSuspended:YES];
            
            // Request version async:
            [self getDetailsOfObjectWithId:object.identifier
                         completionHandler:^(NSString * _Nullable version) {
                NSLog(@"Version = %@", version);
                
                NSLog(@"_myQueue.operationCount = %lu", (unsigned long) _myQueue.operationCount);
                
                // When operations are pending, start the next:
                if (_myQueue.operationCount > 0) {
                    [_myQueue setSuspended:NO];
                }
                else {
                    // Queue is complete
                    NSLog(@"All %lu versions have been requested.", (unsigned long)[_array count]);
                }
            }];
        }];
    }
}

Notice that operationCount is used in the completion handler to know when all operations are completed (instead of Key Value Observer).

The Console Log shows requesting the version of 12 objects in an array. The queue is executed one by one, which is the desired result:

2021-04-10 15:02:01.911996+0200  getDetailsOfObjectWithId:completionHandler:
2021-04-10 15:02:02.001955+0200  Version = 4.1.10
2021-04-10 15:02:02.002091+0200  _myQueue.operationCount = 11
2021-04-10 15:02:02.002292+0200  getDetailsOfObjectWithId:completionHandler:
2021-04-10 15:02:02.108418+0200  Version = 1.0.18
2021-04-10 15:02:02.108611+0200  _myQueue.operationCount = 10
2021-04-10 15:02:02.108844+0200  getDetailsOfObjectWithId:completionHandler:
2021-04-10 15:02:02.201625+0200  Version = 0.0.85
2021-04-10 15:02:02.201810+0200  _myQueue.operationCount = 9
2021-04-10 15:02:02.202048+0200  getDetailsOfObjectWithId:completionHandler:
2021-04-10 15:02:02.289626+0200  Version = 3.1.0
2021-04-10 15:02:02.289851+0200  _myQueue.operationCount = 8
2021-04-10 15:02:02.290140+0200  getDetailsOfObjectWithId:completionHandler:
2021-04-10 15:02:02.369086+0200  Version = 2.0.2
2021-04-10 15:02:02.369295+0200  _myQueue.operationCount = 7
2021-04-10 15:02:02.369525+0200  getDetailsOfObjectWithId:completionHandler:
2021-04-10 15:02:02.444134+0200  Version = 1.0.11
2021-04-10 15:02:02.444270+0200  _myQueue.operationCount = 6
2021-04-10 15:02:02.444386+0200  getDetailsOfObjectWithId:completionHandler:
2021-04-10 15:02:02.513550+0200  Version = 4.0.0
2021-04-10 15:02:02.513741+0200  _myQueue.operationCount = 5
2021-04-10 15:02:02.513952+0200  getDetailsOfObjectWithId:completionHandler:
2021-04-10 15:02:02.600841+0200  Version = 1.2.4
2021-04-10 15:02:02.601030+0200  _myQueue.operationCount = 4
2021-04-10 15:02:02.601243+0200  getDetailsOfObjectWithId:completionHandler:
2021-04-10 15:02:02.691918+0200  Version = 7.0.2
2021-04-10 15:02:02.692064+0200  _myQueue.operationCount = 3
2021-04-10 15:02:02.692242+0200  getDetailsOfObjectWithId:completionHandler:
2021-04-10 15:02:02.777012+0200  Version = 3.1.81
2021-04-10 15:02:02.777116+0200  _myQueue.operationCount = 2
2021-04-10 15:02:02.777244+0200  getDetailsOfObjectWithId:completionHandler:
2021-04-10 15:02:02.864673+0200  Version = 1.0.12
2021-04-10 15:02:02.864851+0200  _myQueue.operationCount = 1
2021-04-10 15:02:02.865050+0200  getDetailsOfObjectWithId:completionHandler:
2021-04-10 15:02:02.961894+0200  Version = 1.0.7
2021-04-10 15:02:02.962073+0200  _myQueue.operationCount = 0
2021-04-10 15:02:02.962226+0200  All 12 versions have been requested.
like image 22
Martijn Avatar answered Nov 02 '22 19:11

Martijn