How to Wait for Method That Has Completion Block (All on Main Thread)

How to wait for method that has completion block (all on main thread)?

If your completion block is also called on the Main Thread, it might be difficult to achieve this, because before the completion block can execute, your method need to return. You should change implementation of the asynchronous method to:

  1. Be synchronous.

    or
  2. Use other thread/queue for completion. Then you can use Dispatch Semaphores for waiting. You initialize a semaphore with value 0, then call wait on main thread and signal in completion.

In any case, blocking Main Thread is very bad idea in GUI applications, but that wasn't part of your question. Blocking Main Thread may be required in tests, in command-line tools, or other special cases. In that case, read further:


How to wait for Main Thread callback on the Main Thread:

There is a way to do it, but could have unexpected consequences. Proceed with caution!

Main Thread is special. It runs +[NSRunLoop mainRunLoop] which handles also +[NSOperationQueue mainQueue] and dispatch_get_main_queue(). All operations or blocks dispatched to these queues will be executed within the Main Run Loop. This means, that the methods may take any approach to scheduling the completion block, this should work in all those cases. Here it is:

__block BOOL isRunLoopNested = NO;
__block BOOL isOperationCompleted = NO;
NSLog(@"Start");
[self performOperationWithCompletionOnMainQueue:^{
NSLog(@"Completed!");
isOperationCompleted = YES;
if (isRunLoopNested) {
CFRunLoopStop(CFRunLoopGetCurrent()); // CFRunLoopRun() returns
}
}];
if ( ! isOperationCompleted) {
isRunLoopNested = YES;
NSLog(@"Waiting...");
CFRunLoopRun(); // Magic!
isRunLoopNested = NO;
}
NSLog(@"Continue");

Those two booleans are to ensure consistency in case of the block finished synchronously immediately.

In case the -performOperationWithCompletionOnMainQueue: is asynchronous, the output would be:

Start

Waiting...

Completed!

Continue

In case the method is synchronous, the output would be:

Start

Completed!

Continue

What is the Magic? Calling CFRunLoopRun() doesn’t return immediately, but only when CFRunLoopStop() is called. This code is on Main RunLoop so running the Main RunLoop again will resume execution of all scheduled block, timers, sockets and so on.

Warning: The possible problem is, that all other scheduled timers and block will be executed in meantime. Also, if the completion block is never called, your code will never reach Continue log.

You could wrap this logic in an object, that would make easier to use this pattern repeatedy:

@interface MYRunLoopSemaphore : NSObject

- (BOOL)wait;
- (BOOL)signal;

@end

So the code would be simplified to this:

MYRunLoopSemaphore *semaphore = [MYRunLoopSemaphore new];
[self performOperationWithCompletionOnMainQueue:^{
[semaphore signal];
}];
[semaphore wait];

how to block the main thread to wait until completion block is over

You don't want to block the main thread, it will be bad for you and the user.

Instead, in the block, once you have the image data and the MD5 ready, you should switch back to the main thread (using dispatch_async or performSelectorOnMainThread...) and update your UI with the new data. You can either store the image data in an instance variable before doing that or pass it as a parameter to the method you call (the main thread block will capture it if you use the dispatch approach).

In this way you are embracing the asynchronous nature of what you're trying to do and utilising the multi-threading capability of iOS to make life good for your user.

Ensuring completion block is executed on the main thread

That depends entirely on the implementation of the actual method someLongNetworkOperationWithCompletionBlock:. Whatever queue it calls the parameter block on is the queue it will run on. If you are executing code on a background queue in that method and want it to always call the block on main, put the GCD call in that method directly.

The implementation of a method that does this would look something like this:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^()
{
// execute code on background queue
dispatch_async(dispatch_get_main_queue(), ^()
{
// call completion block on main
completion();
});
});

Waiting until the task finishes

If you need to hide the asynchronous nature of myFunction from the caller, use DispatchGroups to achieve this. Otherwise, use a completion block. Find samples for both below.


DispatchGroup Sample

You can either get notified when the group's enter() and leave() calls are balanced:

func myFunction() {
var a = 0

let group = DispatchGroup()
group.enter()

DispatchQueue.main.async {
a = 1
group.leave()
}

// does not wait. But the code in notify() is executed
// after enter() and leave() calls are balanced

group.notify(queue: .main) {
print(a)
}
}

or you can wait:

func myFunction() {
var a = 0

let group = DispatchGroup()
group.enter()

// avoid deadlocks by not using .main queue here
DispatchQueue.global(qos: .default).async {
a = 1
group.leave()
}

// wait ...
group.wait()

print(a) // you could also `return a` here
}

Note: group.wait() blocks the current queue (probably the main queue in your case), so you have to dispatch.async on another queue (like in the above sample code) to avoid a deadlock.


Completion Block Sample

func myFunction(completion: @escaping (Int)->()) {
var a = 0

DispatchQueue.main.async {
let b: Int = 1
a = b
completion(a) // call completion after you have the result
}
}


// on caller side:
myFunction { result in
print("result: \(result)")
}

How to make loop wait until block has finished?

The problem is, is the loop runs and causes all of the blocks to trigger, then they are all loading at the same time

That is not a problem, it is good - the reason why the call is asynchronous is it can take an arbitrarily long time to complete. If you've got multiple downloads to do then doing then concurrently can be a big win.

thus the imageArray will result in a different order to the original array of PFFiles, because if one object finishes loading before the previous object, it will be added to the array before the previous one, making it go out of order.

This is the problem and can be addressed in a number of ways.

As you are using arrays, here is a simple array based solution: first create yourself an array of the right size and fill it with nulls to indicate the image hasn't yet arrived:

(all code typed directly into answer, treat as pseudo-code and expect some errors)

NSUInteger numberOfImages = pictureArray.length;

NSMutableArray *downloadedImages = [NSMutableArray arrayWithCapacity:numberOfImages];
// You cannot set a specific element if it is past the end of an array
// so pre-fill the array with nulls
NSUInteger count = numberOfImages;
while (count-- > 0)
[downloadedImages addObject:[NSNull null]];

Now you have your pre-filled array just modify your existing loop to write the downloaded image into the correct index:

for (NSUInteger ix = 0; ix < numberOfImages; ix++)
{
PFFile *picture = pictureArray[ix];
[picture getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
dispatch_async(dispatch_get_main_queue(),
^{ [imageArray replaceObjectAtIndex:ix
withObject:[UIImage imageWithData:data]
});
[self savePhotos];
}];
}

The use of dispatch_async here is to ensure there are not concurrent updates to the array.

If you wish to know when all images have been downloaded you can check for that within the dispatch_async block, e.g. it can increment a counter safely as it is running on the main thread and call a method/issue a notification/invoke a block when all the images have downloaded.

You are also possibly making things harder on yourself by using arrays, and trying to keep items in different arrays related by position. Dictionaries could save you some of the hassle, for example each of your PFFile objects presumably relates to a different URL, and a URL is a perfectly valid key for a dictionary. So you could do the following:

NSMutableDictionary *imageDict = [NSMutableDictionary new];

for (PFFile *picture in pictureArray) {
[picture getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
dispatch_async(dispatch_get_main_queue(),
^{ [imageDict setObject:[UIImage imageWithData:data] forKey:picture.URL];
};
[self savePhotos];
}];
}

And your can locate the image for a PFFile instance by looking up its URL in the dictionary - which will return nil if the image is not yet loaded.

There are other solutions, and you might want to look into making your code more asynchronous. Whatever you do trying to call asynchronous code synchronously is not a good idea and will impact the user experience.

HTH

Why do we call completion on main thread in asynchronous function?

Yes, but it's often convenient to call completion handlers on the main queue, since it's common to want to update the UI, and calling UI updaters on the wrong queue is a common bug.

It is not required, nor even universally recommended, to make general-purpose async functions call back on the main queue. But for certain systems where the use cases are well-known, it's very convenient and helps prevent bugs.

The above example, of course, is not useful, and would be very unlikely to be found in production code. But that doesn't change the utility in specialized cases. Another common pattern is for the caller to pass a completion queue. That's also fine, particularly for very general-purpose tools (such as URLSession, which uses this approach).

Does main thread wait for child thread completion?

From Thread.IsBackground documentation:

Background threads are identical to foreground threads, except that background threads do not prevent a process from terminating. Once all foreground threads belonging to a process have terminated, the common language runtime ends the process.

And

By default, the following threads execute in the foreground (that is, their
IsBackground property returns false):

  • The primary thread (or main application thread).
  • All threads created by calling a Thread class constructor.

You are creating foreground thread which prevents your process from terminating.

Also note that working with threads directly since the introduction of TPL is usually not recommended but if there is a particular reason making you to use them you can set the IsBackground property to true to allow the process to terminate:

var thread = new Thread(() =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
Console.WriteLine("Thread complete");
});
thread.IsBackground = true;
thread.Start();


Related Topics



Leave a reply



Submit