Best practice for replacing deprecated sem_init/sem_wait in a cross-platform threading layer on macOS (arm64)

Hi all,

I'm working on a cross-platform runtime that manages a pool of threads (think game engine / emulator style... dozens of guest threads mapped 1:1 to host pthreads). It was originally written for Linux and Windows and we're now porting to macOS on Apple Silicon.

We've hit a wall with a deadlock on macOS and traced it back to our use of POSIX unnamed semaphores (sem_init / sem_wait / sem_post) for thread suspend and resume. We were unaware these have never actually been implemented on macOS, sem_init silently returns -1 with ENOSYS and then sem_wait just hangs forever. That explains our deadlock.

The tricky part is how we use them. Our suspend mechanism works by sending SIGUSR1 to a target thread via pthread_kill. The signal handler then calls sem_wait to block the thread in place until another thread calls sem_post to resume it. So whatever we replace sem_init/sem_wait with needs to be safe to call from inside a signal handler.

From what I can tell:

dispatch_semaphore_wait is not documented as async-signal-safe pthread_cond_wait is also not async-signal-safe os_sync_wait_on_address looks promising but requires macOS 14.4+ which is a pretty high floor We could spin on a std::atomic<bool> with .wait() / .notify_all() but I've seen reports of high wake latency (up to 15ms) in libc++'s implementation on macOS My questions:

What's the recommended way to block a thread inside a signal handler on macOS? Is there an async-signal-safe wait primitive I'm missing? Would restructuring to avoid blocking in the signal handler entirely be the better approach? For example, having the signal handler just set an atomic flag and then checking it at yield points — would that be the expected pattern on macOS? For the non-signal-handler suspend/resume paths, is dispatch_semaphore_t the right replacement for sem_t, or is there something better suited for high-frequency thread synchronization in 2026? Separately, we're also using ucontext (makecontext/swapcontext) for a fiber system on macOS and hitting issues on native arm64, it works under Rosetta but breaks natively. We have a setjmp/longjmp + manual stack pivot backend we can switch to. Is there any plan to fix or un-deprecate the ucontext functions on arm64, or should we just move off them permanently?

What's the recommended way to block a thread inside a signal handler on macOS?

So, my initial reaction here is "don't". The Objective-C and Swift runtimes both involve locking (not to mention all of the other locks in the system), so blocking in a signal handler has a HIGH probability of creating deadlocks. It's possible you're dealing with an isolated environment/language that avoids these issues, but avoiding nearly ALL system APIs is a fairly high bar.

Would restructuring to avoid blocking in the signal handler entirely be the better approach?

Yes, that would be my inclination. Keep in mind that mach is the system's primary IPC mechanism, which means that, in general, signals aren't used to nearly the same degree they are on other platforms.

For example, having the signal handler just set an atomic flag and then checking it at yield points — would that be the expected pattern on macOS?

That's certainly a more common pattern; however, if you're going that far, there's no reason not to just remove the signal handler entirely.

For the non-signal-handler suspend/resume paths, is dispatch_semaphore_t the right replacement for sem_t, or is there something better suited for high-frequency thread synchronization in 2026?

I think this depends almost entirely on what you're actually trying to do. The system’s general approach has been to move away from using threads as individual execution streams in favor of using dispatch queues to share threads. That's a very different execution model than you’re describing.

Is there any plan to fix or un-deprecate the ucontext functions on arm64, or should we just move off them permanently?

While it's traditional for DTS to avoid commenting on our future plans, I'm willing to say that it's very unlikely that these functions will ever return.

The background here is that the ucontext function set was ALREADY marked obsolete in the Open Group Base Specifications Issue 6... in 2004:

https://pubs.opengroup.org/onlinepubs/007904875/functions/setcontext.html

APPLICATION USAGE

The obsolescent functions getcontext(), makecontext(), and swapcontext() can be replaced using POSIX threads functions.

If you look at our code, it's fairly clear that they were implemented for the conformance tests, not because we actually thought they were a good idea. In any case, if you look at the latest specification (Issue 7 - 2018), you'll see that we didn't "remove" them; the opengroup did:

https://pubs.opengroup.org/onlinepubs/9699919799/

The ARM transition simply provided a convenient opportunity to complete their removal.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Best practice for replacing deprecated sem_init/sem_wait in a cross-platform threading layer on macOS (arm64)
 
 
Q