How to safely use [NSTask waitUntilExit] off the main thread?
Asked Answered
C

2

5

I have a multithreaded program that needs to run many executables at once and wait for their results.

I use [nstask waitUntilExit] in an NSOperationQueue that runs it on non-main thread (running NSTask on the main thread is completely out of the question).

My program randomly crashes or runs into assertion failures, and the crash stacks always point to the runloop run by waitUntilExit, which executes various callbacks and handlers, including—IMHO incorrectly—KVO and bindings updating the UI, which causes them to run on non-main thread (It's probably the problem described by Mike Ash)

How can I safely use waitUntilExit?

Is it a problem of waitUntilExit being essentially unusable, or do I need to do something special (apart from explicitly scheduling my callbacks on the main thread) when using KVO and IB bindings to prevent them from being handled on a wrong thread running waitUntilExit?

Cynthea answered 25/1, 2016 at 16:2 Comment(3)
One question is: why are you using -waitUntilExit? Why not launch the task from the main thread but, rather than blocking waiting for it to exit, add an observer for NSTaskDidTerminateNotification and do whatever work you want there? You will have to maintain a strong reference to the task object for the duration, but that's easy enough.Fidget
@KenThomases because synchronous NSOperation is easier to manage than async one. With events and callbacks my ObjC code starts to look like node.js.Cynthea
In my case, I call waitUntilExit only if isRunning is true. Solved my crashesHertha
O
8

As Mike Ash points out, you just can't call waitUntilExit on a random runloop. It's convenient, but it doesn't work. You have to include "doesn't work" in your computation of "is this actually convenient?"

You can, however, use terminationHandler in 10.7+. It does not pump the runloop, so shouldn't create this problem. You can recreate waitUntilExit with something along these lines (untested; probably doesn't compile):

dispatch_group group = dispatch_group_create();
dispatch_group_enter(group);
task.terminationHandler = ^{ dispatch_group_leave(group); };
[task launch];
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

// If not using ARC:
dispatch_release(group);
Ozonide answered 25/1, 2016 at 22:20 Comment(4)
Napier strikes again, this saved me so much trouble from all these weird crashes from random background thread async operations. Good to know how to force synchronous operations now too if necessaryPrecambrian
@Rob Napier -- do you know if the completion handler is guaranteed to be invoked on the same thread that launched the NSTask? I can't find that promise anywhere in the documentation and since many of Apple's other completion handler APIs run on any thread they want, it's worrying.Edbert
@bryan I would not bet on any particular thread or queue for terminationHandler. The docs specifically say "This block is not guaranteed to be fully executed prior to waitUntilExit returning," which suggests that terminationHandler is not promised to be executed on the same thread as waitUntilExit. Moreover, I don't know how that could even be reliably implemented; NSTask doesn't always require a run loop (and long-predates DispatchQueues). There's no general way to run a block on an arbitrary thread (-performSelector:onThread:withObject:waitUntilDone: requires a run loop).Ozonide
@Rob Thanks. I can confirm that the termination block runs on arbitrary threads. What's much worse is that there's no guarantee that readability handlers attached to Stdout and Stdin will finish handling data from the task before the completionHandler is called. That's a huge design flaw, in my opinion, because the task isn't complete until all its output data has been handled. Thus far, using a semaphore is the only workaround I can find.Edbert
P
0

Hard to say without general context of what are you doing...

In general you can't update interface from the non main threads. So if you observe some KVO notifications of NSTasks in non main thread and update UI then you are wrong.

In that case you can fix situation by simple

-[NSObject performSelectorOnMainThread:];

or similar when you want to update UI.

But as for me more grace solution:

  1. write separated NSOperationQueue with maxConcurentOperationsCount = 1 (so FIFO queue) and write subclass of NSOperation which will execute NSTask and update UI through delegate methods. In that way you will control amount of executing tasks in application. (or you may stop all of them or else)

  2. But high level solution for your problem I think will be writing privileged helper tool. Using this approach you will get 2 main benefits: your NSTask's will be executes in separated process and you will have root privilegies for executing your tasks.

I hope my answer covers your problem.

Paraselene answered 25/1, 2016 at 16:46 Comment(1)
The problem I have is that callbacks scheduled on the main thread don't run on the main thread, because waitUntilExit dequeues them wherever it is.Cynthea

© 2022 - 2024 — McMap. All rights reserved.