Timed-Wait for main thread

The scenario is, in a macOS app (primarly), main thread needs to wait for some time for a certain 'event'. When that event occurs, the main thread is signaled, it gets unblocked and moves on.
An example is, during shutdown, a special thread known as shutdown thread waits for all other worker threads to return (thread join operation). When all threads have returned, the shutdown thread signals the main thread, which was waiting on a timer, to continue with the shutdown flow. If shutdown thread signals the main thread before the later's timer expires, it means all threads have returned. If main thread's timer expires first, it means some threads have failed to join (probably stuck in infinite loop due to bug, disk I/O etc.).
This post is to understand how main thread can wait for some time for the shutdown thread. There are two ways: a) dispatch_semaphore_t b) pthread conditional variable (pthread_cond_t) and mutex (pthread_mutex_t).
Expanding a bit on option (b) using conditional variable and mutex:

// This method is invoked on the main thread
bool ConditionSignal::TimedWait() noexcept
{
    struct timespec ts;
    pthread_mutex_t * mutex = (pthread_mutex_t *) (&vPosix.vMutexStorage[0]);
    pthread_cond_t * cond  = (pthread_cond_t *) (&vPosix.vCondVarStorage[0]);
    
    // Set the timer to 3 sec.
    clock_gettime(CLOCK_REALTIME, &ts);
    ts.tv_sec += 3;
    
    pthread_mutex_lock(mutex);
    LOG("Main thread has acquired the mutex and is waiting!");
    
    int wait_result = pthread_cond_timedwait(cond, mutex, &ts);
    
    switch (wait_result) {
        case 0:
            LOG("Main thread signaled!");
            return true;
        case ETIMEDOUT:
            LOG("Main thread's timer expired!");
            return false;
        default:
            LOG("Error: Unexpected return value from pthread_cond_timedwait: " + std::to_string(wait_result));
            break;
    }
    
    return false;
}

// This method is invoked on shutdown thread after all worker threads have returned.
void ConditionSignal::Raise() noexcept
{
    pthread_mutex_t * mutex = (pthread_mutex_t *) (&vPosix.vMutexStorage);
    pthread_cond_t *  cond = (pthread_cond_t *) (&vPosix.vCondVarStorage);

    pthread_mutex_lock(mutex);
    LOG("[Shutdown thread]: Signalling main thread...");
    
    pthread_cond_signal(cond);
    pthread_mutex_unlock(mutex);
}

Both options allow the main thread to wait for some time (for shutdown thread) and continue execution. However, when using dispatch_semaphore_t, I get the following warning:

Thread Performance Checker: Thread running at User-interactive quality-of-service class waiting on a lower QoS thread running at Default quality-of-service class. Investigate ways to avoid priority inversions

Whereas, with conditional variables, there are no warnings. I understand the warning - holding the main thread can prevent it from responding to user events, thus causing the app to freeze. And in iOS, the process is killed if main thread takes more than 5 secs to return from applicationWillTerminate(_:) delegate method. But in this scenario, the main thread is on a timed-wait, for some milliseconds i.e., it is guaranteed to not get blocked indefinitely.
While this is described for macOS, this functionality is required for all Apple OSes. What is the recommend way?

Answered by DTS Engineer in 876833022
Whereas, with conditional variables, there are no warnings.

I wouldn’t read too much into that. These warnings have to be explicitly coded, and we added that code for Dispatch semaphores because that’s the most common offender here. It wouldn’t surprise me if we added a similar warning for pthread condition variables at some point in the future. Then again, it wouldn’t surprise me if we didn’t. The pthreads API is relatively obscure.

What is the recommend way?

There isn’t a supported way to block the main thread of a GUI application for long periods of time. On macOS it will SPOD, which is a terrible user experience. On other platforms the app will likely end up being killed by the watchdog.

And to be clear, the issue here isn’t about the specific APIs you’re using, it’s this overall design.

Speaking of iOS and friends, app termination [1] behaves very differently over there. Most apps don’t receive an explicit termination notification. Rather, the user moves the app to the background, at which point the system suspend it, and then at some point in the future the system terminates it by simple removing it from memory, without ever resuming it. So, if you need to clean things up, you have to do that on moving to the background, because there’s no guarantee that your termination code will run.

Moreover, the user can move your app between the background and foreground often, so that code need to be lightweight.

And that speaks to my preferred solution to this problem, which is to structure your app so that it doesn’t rely on this sort of clean up. For example, instead of syncing data structures to disk on quit, you sync them incrementally as the user works. And you use atomic transactions to ensure that the data structure is in a consistent state no matter when your app gets terminated.

Note This design also prevents problems if your app crashes, and if the user force quits your app [2].

An approach like this is pretty much the only one that works on iOS and its child platforms. On macOS you do have one other option, namely to defer app termination. Check out the applicationShouldTerminate(_:) app delegate method. In that case, however, you can’t simple block the main thread. Rather, AppKit continues to run the main thread’s run loop until you call reply(toApplicationShouldTerminate:).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] I presume that’s what you mean by “shutdown”. On Apple platforms we generally use shutdown to refer to the system as a whole shutting down. Apps are quit (user term) or terminated (developer term).

[2] Something that’s super common iOS, for annoying reasons.

Whereas, with conditional variables, there are no warnings.

I wouldn’t read too much into that. These warnings have to be explicitly coded, and we added that code for Dispatch semaphores because that’s the most common offender here. It wouldn’t surprise me if we added a similar warning for pthread condition variables at some point in the future. Then again, it wouldn’t surprise me if we didn’t. The pthreads API is relatively obscure.

What is the recommend way?

There isn’t a supported way to block the main thread of a GUI application for long periods of time. On macOS it will SPOD, which is a terrible user experience. On other platforms the app will likely end up being killed by the watchdog.

And to be clear, the issue here isn’t about the specific APIs you’re using, it’s this overall design.

Speaking of iOS and friends, app termination [1] behaves very differently over there. Most apps don’t receive an explicit termination notification. Rather, the user moves the app to the background, at which point the system suspend it, and then at some point in the future the system terminates it by simple removing it from memory, without ever resuming it. So, if you need to clean things up, you have to do that on moving to the background, because there’s no guarantee that your termination code will run.

Moreover, the user can move your app between the background and foreground often, so that code need to be lightweight.

And that speaks to my preferred solution to this problem, which is to structure your app so that it doesn’t rely on this sort of clean up. For example, instead of syncing data structures to disk on quit, you sync them incrementally as the user works. And you use atomic transactions to ensure that the data structure is in a consistent state no matter when your app gets terminated.

Note This design also prevents problems if your app crashes, and if the user force quits your app [2].

An approach like this is pretty much the only one that works on iOS and its child platforms. On macOS you do have one other option, namely to defer app termination. Check out the applicationShouldTerminate(_:) app delegate method. In that case, however, you can’t simple block the main thread. Rather, AppKit continues to run the main thread’s run loop until you call reply(toApplicationShouldTerminate:).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] I presume that’s what you mean by “shutdown”. On Apple platforms we generally use shutdown to refer to the system as a whole shutting down. Apps are quit (user term) or terminated (developer term).

[2] Something that’s super common iOS, for annoying reasons.

Timed-Wait for main thread
 
 
Q