I have a bundled macOS application. This is a non-interactive application where I m performing some task on the worker thread while the main thread waits for this task to be completed. Sometimes this task can be time consuming.
I have observed that when I run the application using the bundle( like double click or open command) I can see the OS marking my application as not responding( this is evident as the app icon toggles in the dock and then it states not responding).
Although If I run the unix executable in the bundle, the app runs and I do not see the not responding status anywhere.
I wanted to understand If this is happening because my main thread is in a waiting state? If yes, what could I do to resolve it because my application logic demands the main thread to wait for the worker thread to complete its task. Is there some way to use some event loop like GCD?
Note: I cannot use the delegates(Appkit) event loop because my application will be run in non-GUI context.
First off, we need to clarify the vocabulary here, as the term "context" has a very specific meaning in macOS that's much narrower than what you're using. What I'd actually recommend here is that you start by reading the timeless classic "TN2083: Daemons and Agents", which lays out how execution contexts work in macOS.
However, the critical point here is that what "user context" means in macOS is that a process is part of the users login session/execution environment. Case in point, all of these cases:
-
Double click on the app.
-
Open up Terminal.app and run the app through "open".
-
Open up Terminal.app and run the executable directly.
...are running in the "user context". I think what's confused the issue here is that user context isn't defined by "what" you are (like GUI vs. non-GUI). The "open" command line tool is actually a great example of this- open only really works when run inside the user context because what it actually does (use LaunchServices to "open stuff") is inherently tied to the user interface.
With that background, let me jump back to here:
Note: I cannot use the delegates(Appkit) event loop because my application will be run in non-GUI context.
Is that actually true? If so, how/why is it being run that way? This would typically be done using a launch daemon, but it would be pretty odd to have a launch daemon that was also a double clickable app.
Moving back to the question here:
I wanted to understand If this is happening because my main thread is in a waiting state?
Sort of. More specifically, the system expects "apps" to have a functioning main runloop it can deliver events to. Apps are then marked as "not responding" if their main runloop is blocked for to long or, as in your case, non-existant.
Quick answer to here:
Is there some way to use some event loop like GCD?
I don't think GCD itself will work here but I haven't actually tried. dispatch_main exists to provide daemon's that operate below CoreFoundation with an architecture option similar to the runloop, however, I don't think it will satisfy the WindowServer.
If yes, what could I do to resolve it because my application logic demands the main thread to wait for the worker thread to complete its task.
Two different options here:
a) You may want to consider reworking your code into your core "tool" (which will be a pure-command line tool) and your "app" component (which is what would run when launched the app was double clicked). Your app component would then execute your command line tool to do whatever needed to be done. The advantage of this approach is that it makes it easy for give the user other interaction options, even if you don't want to create a full app. Things using the app icon to provide progress/status information, a dock menu for minor interactions, or simply posting information to the user through notifications. Keep in mind that running "as an app" doesn't mean your app has a menu bar or any windows. You can basically make this do whatever you want.
For most use case this is the right choice, since having an app you can launch but that doesn't actually DO anything isn't all that useful.
b) If you're ONLY goal is to prevent the "not responding" status, then you can use a very minimal main that runs the runloop "itself" to do that. Here's a very simply implementation I just threw together:
int main(int argc, const char * argv[]) {
@autoreleasepool {
[NSTimer scheduledTimerWithTimeInterval: 0.1 repeats: YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"Yo");
}];
while(1) {
[[NSRunLoop mainRunLoop] run];
}
}
return 0;
}
Note that the timer is actually doing two different things here:
-
(minor) It makes it easy to confirm that the code actually "works" and is running.
-
(major) All of NSRunLoop's "run" methods immediately return unless they have "something" to do, so having the timer scheduled is what actually cause "run" to block here. In real usage you'd typically use the main thread for things like timers, callbacks, etc., which you'd setup before you called run. However, if you don't have ANY usage for the main thread, then you can also schedule the timer arbitrarily far in the future and then do all of your work on a background thread (which you'd kick off before calling "run"). What matters here is that the timer is scheduled, not that it actually runs.
One more detail- the code above will never exit on it's own because run will never return. While it's technically possible to make it return by ending whatever work you're doing, in practice that doesn't actually work all that well and isn't worth the trouble. If you're using this architecture, you should just call "exit" whenever you've finished your work, which is exactly with all our frameworks do.
-Kevin