Polling GCD main queue, to avoid deadlock

230 views Asked by At

I have an iOS app, with multiple threads. In a background thread, I run some 3rd party code. The 3rd party code will occasionally call:

dispatch_sync(dispatch_get_main_queue(), block);

The callback has to be _sync, because it needs the answer and it needs to be on the main thread, because it calls UIApplication.

The problem occurs when I need to shut down the background thread. The shutdown originates from the UI and it also has to be sync. So, I sometimes see a deadlock.

I have tried to solve it, by the method described here. Basically calling the NSRunLoop in a loop, until a flag has been set by the background thread. Like this:

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];

However, it does not work. I can see in the debugger that the background thread is hanging on the dispatch_sync call, even though the NSRunLoop runUntilDate is called multiple times.

Here is the callstack of the background thread, which is blocked:

enter image description here

And here is the callstack of the main thread (it is not crashed, merely paused in the debugger):

enter image description here

I think it must be something about the runmodes, but I am not sure what and how to fix that. So how can I service the GCD main queue, while the main thread is busy in a loop?

1

There are 1 answers

0
Rob On

That NSRunLoop technique can work in some situations:

- (void)start {
    dispatch_sync(self.queue, ^{
        [self finishInTenSeconds];
        while (!self.isFinished) {
            NSLog(@"loop");
            [[NSRunLoop mainRunLoop] runUntilDate:[[NSDate date] dateByAddingTimeInterval:1]];
        }
    });
}

- (void)finishInTenSeconds {
    [NSTimer scheduledTimerWithTimeInterval:10 repeats:false block:^(NSTimer * _Nonnull timer) {
        self.isFinished = true;
    }];
}

But this technique of manually calling run on the main run loop while simultaneously blocking the main thread, is essentially a kludge (and a bit of an anachronism). E.g. replace the timer in finishInTenSeconds with GCD call, and this closure will never get called:

- (void)finishInTenSeconds {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.isFinished = true;
    });
}

The problem is that the while loop with the run call will allow the run loop to process events, but it doesn’t alter the fact that the main thread is blocked by the original dispatch_sync call.

Note, the particulars in your case are undoubtedly different, but hopefully this illustrates the general class of problem introduced with the NSRunLoop kludge.


The appropriate solution is to minimize synchronous calls. E.g. if you need to do something after the task on the background queue finishes, rather than calling synchronously, like so:

dispatch_sync(self.queue, ^{
    [self foo];
});

[self bar];

You would instead dispatch asynchronously, and then, when it’s done, dispatch the subsequent code asynchronously back to the main queue:

dispatch_async(self.queue, ^{
    [self foo];

    dispatch_async(dispatch_get_main_queue(), ^{
        [self bar];
    });
});