简体   繁体   中英

Objective-C dispatch method with block that will run on the *caller* thread

I write a black box class that does heavy processing in the background using Grand Central Dispatch. I intend to provide a continuation style API, something like:

- (void) processHeavyStuff:(id) someParam thenDo:(ContinuationBlock)followup;

Which a client could call for example like this:

[myBlackBox processHeavyStuff:heavyOne thenDo: ^(Dalek* result){
    [self updateDisplayWithNewDalek:result];
}];

What's commonly done is that the processHeavyStuff:thenDo: implementation calls its continuation block on the main thread using dispatch_get_main_queue() . See Invoke model method with block that will run on the main thread for an example.

However this common scenario assumes the client is calling from the main thread. I would like to be more general and call the continuation block on the caller's thread, which may or may not be the main thread. This would allow for example to play nice with Core Data threaded clients where the NSManagedObjectContext is thread-local. Is there a good pattern for that?

Using –[NSObject performSelector:onThread:withObject:waitUntilDone:] , I can see I could define the ancillary method:

- (void) callContinuation:(ContinuationBlockWithNoArgument) followup
{
    followup();
}

And then perform that selector on the caller's thread:

- (void) processHeavyStuff:(id) someParam thenDo:(ContinuationBlock)followup
{
    NSSthread *callerThread = [NSThread currentThread];

    dispatch_async(self.backgroundQueue, ^ {
        Dalek *newDalek = [self actuallyDoTheHeavyProcessing:someParam];

        [self performSelector:@selector(callContinuation:) onThread:callerThread
            withObject: ^{
                followup(newDalek);
            }
            waitUntilDone:NO];
    });
}

I guess that could work, and I am going to try it. But is there something less contrived? Perhaps a version of performSelector:onThread: for blocks?

PS: For clarity purposes, I left all memory management calls out of the above snippets. For example, the followup block is stack-based and must be copied to the heap to use it on another thread...

Edit: I found out that Mike Ash uses a very similar approach with:

void RunOnThread(NSThread *thread, BOOL wait, BasicBlock block)
{
    [[[block copy] autorelease] performSelector: @selector(my_callBlock) onThread: thread withObject: nil waitUntilDone: wait];
}

Where my_callBlock is defined in a category on NSObject :

@implementation NSObject (BlocksAdditions)
- (void)my_callBlock
{
    void (^block)(void) = (id)self;
    block();
}
@end;

dispatch_get_current_queue() returns the current queue which is the caller queue when called at the beginning of your method. Change your code to:

- (void)processHeavyStuff:(id)someParam thenDo:(ContinuationBlock)followup {
    dispatch_queue_t callerQueue = dispatch_get_current_queue();
    dispatch_retain(callerQueue);

    dispatch_async(self.backgroundQueue, ^ {
        Dalek *newDalek = [self actuallyDoTheHeavyProcessing:someParam];

        dispatch_async(callerQueue, ^{
            followUp(dalek);
            dispatch_release(callerQueue);
        });
    });
}

One thing I'm not quite sure about is if you need to retain the callerQueue and release it afterwards. I think you don't.

Hope that helps you!

EDIT : added retain/release

It's a bad idea to try and guess the caller's intention as to where the completion handler block should be executed. You might guess wrong, and bad things will happen.

The queue returned by dispatch_get_current_queue() may be used by the caller in ways you don't expect and cannot anticipate; perhaps they call dispatch_suspend() on it by the time you enqueue your completion handler. You also can't assume you can send performSelector: to the return value of [NSThread currentThread] at some arbitrary point in the future; what if this thread has exited by the time you enqueue the completion handler? What if this thread doesn't have a run loop? In both cases, your completion handler never runs.

It's also contrary to the behaviour of many of Apple's methods/functions that take a completion handler block. Generally, the client supplies a queue directly, or the completion handler is executed on an arbitrary thread, in which case it's the client's responsibility to trampoline over to the correct queue or thread if necessary.

You could simply add another parameter to your -processHeavyStuff:thenDo: method specifying the queue you want the block to run on. This approach is much more flexible and is used in NSNotificationCenter 's -addObserverForName:object:queue:usingBlock: (although it uses NSOperationQueues , but the principle is the same). Just don't forget that you should retain and release the queue you're passing in.

Here's how I handle it. It's flexible, but not very elegant.

Since the question is still unanswered, maybe you are still looking for an answer? If not, could you please provide what is working best for you?

// Continuation block is executed on an arbitrary thread.
// Caller can change context to specific queue/thread in
// the continuation block if necessary.
- (void) processHeavyStuff:(id) someParam thenDo:(ContinuationBlock)followup;

// Continuation block is executed on the given GCD queue
- (void) processHeavyStuff:(id) someParam thenOnQueue:(dispatch_queue_t)queue do:(ContinuationBlock)followup;

// Continuation block is executed on the given thread
- (void) processHeavyStuff:(id) someParam thenOnThread:(NSThread*)thread do:(ContinuationBlock)followup;

// Continuation block is executed on *this* thread
- (void) processHeavyStuff:(id) someParam thenDoOnThisThread:(ContinuationBlock)followup;

// Continuation block is executed on main thread
- (void) processHeavyStuff:(id) someParam thenDoOnMainThread:(ContinuationBlock)followup;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM