Thanks for posting this to HN. I originally published this some time ago when porting a heavily-multithreaded library from Windows to Mac and Linux. It should run on Windows/Linux/Mac/FreeBSD/etc. and it's been vetted and tested over the years. I even came across some backtraces from Steam's Mac/Linux app that indicated they were using my code a few years back, so there's that.
I'm a bit ashamed that I opted to write it in C++ originally. It doesn't have much in terms of standard library dependencies (just dequeue), but in the process of porting it to C I decided to also change how the inner notifications system works with regards to the cleanup of allocated memory for completed waits and now I'm stuck on a particularly nasty decision of whether to add a mutex, use reference counting, or let memory grow until an event is destroyed... and so the code is still C++ :)
Just because synchronization primitives are pretty low level so it would be nice for it to compile as C code. I should just expose the API without name mangling, I suppose.
(I forgot to add: the library is also free of spurious-wakeups, which I feel is massive gotcha in pthreads.)
If you don't need wait-for-multiple-objects I had luck using c++ futures instead of win32 events with similar semantics (calling future's promise set-value instead of signalling the event object).
At some point the Linux kernel started adding stuff that reminds me a lot of Windows synchronization and kernel events. Like eventfd(2).
Combine that with epoll and you have something a little simpler than the code I am reading in this library. (And like win32 events can cross a process boundary.)
Yup, though eventfd is (annoyingly) half-way between an automatic and a manual-reset event. Like auto-reset you can test and reset with a single system call (read(), which can be either blocking or non-blocking). If you use poll() however they are more like manual-reset.
Writing code that is portable between POSIX and Win32 is not easy. Libraries such as this one can be useful, but it's even better if you let both OSes teach you some tricks, sometimes porting POSIX features to Win32 and sometimes the other way round.
For example, manual-reset events are actually a pretty nifty replacement for condition variables in lock-free code. However you can't use them directly because SetEvent/ResetEvent are too heavy-weight in Win32, going down to the kernel even if there's no waiter. So after reimplementing manual-reset events in POSIX, you can apply the lessons you learnt to speed up Win32 as well.
For QEMU we have two very different portability wrappers for Win32 events. One is inspired by eventfd (it uses manual-reset events on Windows and pipes on POSIX systems other than Linux) and is used together with poll() on POSIX systems and WaitForMultipleObjects on Win32 systems. The second is a single-process version of the manual reset event with a fast userspace-only fast path; on POSIX it uses futexes, while on Win32 it uses a manual-reset event but it wraps it to skip as many system calls as possible. Resetting the event in fact doesn't ever go to the kernel (the Win32 code calls ResetEvent just before waiting, it's a bit tricky but not racy).
Another little-known trick we use in QEMU is combining WSAEventSelect, select() and WaitForMultipleObjects to achieve level-triggered polling of sockets and other file descriptors; because we have a bunch of legacy network code we cannot use edge-triggered epoll or WSAEventSelect (on Windows, select is level triggered but only works with sockets; WSAEventSelect and WaitForMultipleObjects are edge triggered but work with other file descriptors). The socket portability codein glib is insanely complicated; it creates a thread per socket, buffers all I/O, and uses a bunch of events for synchronization. In QEMU it was just 100 or so lines of code. Winsock is still a pain for portability, especially if you care about performance a lot, but with the right tricks it's manageable.
Events are nothing more than semaphores that cannot count past 1 and clamp; i.e. "binary semaphores". (At least the auto-reset ones.) Microsoft finally added events that can count past 1 and called them semaphores, under a different API.
If you use this:
HANDLE WINAPI CreateSemaphore(
_In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
_In_ LONG lInitialCount,
_In_ LONG lMaximumCount,
_In_opt_ LPCTSTR lpName
);
and specify a maximum count of 1, you basically get the same thing as an auto-reset event, under a different API.
Linux has had semaphores in the kernel for ages: see sem_up, sem_down.
The "event" term isn't used as a synonym for a semaphore, like in Win32.
Of course Linux has had semaphores for a long time. (As has Windows.) I know my way around the Linux kernel pretty well and on the Windows side, I am ex-msft. So I am not learning anything from your scholarly summary here.
I mean that eventfd(2) is somewhat more recent and reminds me more of Win32 than the POSIX APIs for this. The API exposes an fd (read: HANDLE) which you can block on with the select/poll/epoll syscalls (read: WaitForMultipleObjects). The comparison could not be more direct. Whereas Unixy APIs and internal-to-linux APIs do not historically reason about synchronization primitives in terms of files.
(PS. Manpage for eventfd(2) says you can open it in a semaphore mode.)
They cleaned up the MSDN pages (which seems a huge and sad loss of historical data - they should include a history section like man pages), but have a look at the Japanese page:
It mentions it's supported on Windows 95 and NT 4.0
(On a side note, I'm completely baffled that the metadata for MSDN is not locale-independent In their documentation generation engine/system. I would have thought only the page text would change from one language to another.)
I had to do a bit of programming for NT4 only a few years ago (shudder), and the earliest OS the MSDN docs mentioned was always Windows 2000, even when those functions existed on NT4.
Microsoft isn't very interested in maintaining docs about their ancient OSes.
PulseEvent was identified as harmful more than two decades ago; there is no reason to proliferate it. It only worked meaningfully under cooperative multitasking in 16 bit Windows 3.x. In that OS, one task ran at a time, so it was as if a global mutex were being held at all times.
PulseEvent resembles a condition variable broadcast which atomically gives up the global mutex and wakes up some waiting threads. Under cooperative tasking, if you change some shared variables and then PulseEvent, it is not possible for some thread to see the old values of those variables, and yet miss the wakeup, because no other thread is running. When the implicit global mutex is gone (preemption, SMP), the function loses its reliable meaning.
PulseEvent should not have been used in 32-bit Windows 95 application coding (preemptively multi-tasked) and thereafter. We're nearing the 21 year anniversary of Windows 95; kids that were born when Win95 came out are starting to pop out universities with CS degrees.
I agree with you 100% and then some. Unfortunately (and you can probably see this by going back and looking at the commit history, <3 open source and version control!) you have no idea the number of emails I received about this being an "incomplete solution" and "incompatible with porting existing Win32 code" because of the lack of PulseEvent.
From the README:
-DPULSE: Enables the PulseEvent function. PulseEvent() on Windows is
fundamentally broken and should not be relied upon — it will almost
never do what you think you're doing when you call it. pevents includes
this function only to make porting existing (flawed) code from WIN32 to
*nix platforms easier, and this function is not compiled into pevents by
default.
I'm a bit ashamed that I opted to write it in C++ originally. It doesn't have much in terms of standard library dependencies (just dequeue), but in the process of porting it to C I decided to also change how the inner notifications system works with regards to the cleanup of allocated memory for completed waits and now I'm stuck on a particularly nasty decision of whether to add a mutex, use reference counting, or let memory grow until an event is destroyed... and so the code is still C++ :)