First, the primitive needs to support blocking the caller until some condition occurs. With it, you can then create a new primitive called an Event. This is simply a type where one thread calls event.wait() which suspends its caller until another thread calls event.set(). Think of it as waiting for a bool to become true.
For example, here’s how to do it with POSIX threading primitives: With a Mutex and Condvar it’s pretty straight forward; Wrap a bool in the mutex, using cond.wait() to wait until it’s set and calling cond.signal() when setting it. If there’s only a Mutex available, locking it prematurely turns it into a Semaphore where a subsequent lock() is event.wait() and unlock() is event.set(). RwLock is effectively a Mutex here to the same effect.
For a more practical example, here’s how to build it from OS threading primitives: Most systems provide a Futex API, where wait(&atomic_int, value) blocks until the atomic no longer matches the value and wake(&atomic_int, N) unblocks N threads waiting on the atomic (after its value has presumably been updated). Some OS like Windows and NetBSD instead provide Events directly, with NtWaitForAlertByThreadId/NtAlerThreadByThreadId and lwp_park/lwp_unpark respectively.