The missing cross-platform OS API for timers
33 comments
·February 3, 2025hkwerf
amelius
Another consideration: often you want to wait for several types of object (timer, semaphore, file, etc.), so you need one interface where you can do wait([A, B, C, ...]) where A can be a timer and B a semaphore, C a file, etc. Basically, the OS should present a unified interface.
chikere232
Often such apis (select/poll/epoll) have an option to wait with a timeout, so if you have a bunch of different timers, you can manage them yourself (e.g. on a heap) and set the timeout to the nearest timer
Of course if you have epoll you probably have timerfd, wich does give you a unified interface, just not as portable
russdill
And there's the questions people usually don't think of.
If you clock moves forward or backward, how would you like that the effect your sleep?
If the system suspends during your sleep, do you want that to count towards the sleep? Should expiration wake the system?
oguz-ismail
>usleep
Not since 2008. POSIX has sleep(), nanosleep() and clock_nanosleep() now
hkwerf
Thanks, I've edited the list. :)
Someone
> so setting a timer is equivalent to regularly querying the system time and comparing to a limit
Setting an OS timer would make the thread use 0% CPU. “Regularly querying the system time and comparing to a limit” would not.
Also, a good OS API would allow callers to specify how important it is to be woken at that exact moment, and would use that information to coordinate wake-up times across processes, and thus maximize idle periods.
hkwerf
> Setting an OS timer would make the thread use 0% CPU. “Regularly querying the system time and comparing to a limit” would not.
As mentioned, this is the first case and can be achieved by calls to sleep/Sleep/etc. You'd only regularly query if there is actual useful work to be done. That's the premise of the second case you quoted from.
Joker_vD
The article is, I reckon, about a rather common case when a thread that does multiplexed I/O also needs to perform some timer-related tasks as well. As the very first paragraph states, "A blocking sleep won't cut it!"
Unless you do it in a separate thread and then signal other threads but that's insane.
robertlagrant
Which concerns were not separated?
tom_
For Windows, you can use CreateWaitableTimer to create a timer that doesn't need a window: https://learn.microsoft.com/en-us/windows/win32/api/synchapi...
Wait for it to time out using one of the WaitFor family of functions.
leeter
Alternatively there is everybody's favorite CreateThreadpoolTimer https://learn.microsoft.com/en-us/windows/win32/api/threadpo... now that the threadpool is active on any Win32 process by default AFAIK. But for something you don't mind blocking the above is often easier to use.
tom_
Whew, so we've got CreateWaitableTimer, CreateTimerQueueTimer from another comment, and now CreateThreadpoolTimer as well. But that's not all - we already had WM_TIMER from the article.
TFA says POSIX has one type of timer, and it sucks. Windows has 4 types... but how many of them suck? I've used CreateWaitableTimer, and it was alright. I do know WM_TIMER sucks, but I've never used CreateTimerQueueTimer or CreateThreadpoolTimer. So, worst case is a draw. I'd like to see Windows winning here though.
pjc50
Very minor point: Windows applications only come with a message queue if you explicitly start one, so if you have a headless application it needs to have an invisible "window". However I agree with the author that it is a pretty nice API in general. There are some problems caused by the very limited parameter size, but it's a standard notification API for asynchronous OS events. We use it headlessly for detecting USB insertions.
01HNNWZ0MV43FF
And doesn't Windows have that weird distinction that every exe is either a console application or a GUI application but not both? Makes it hard to get stderr out of GUIs
tom_
Every exe is sort-of both - a GUI program can allocate a console window using AllocConsole (https://learn.microsoft.com/en-us/windows/console/allocconso...), and a console program can create a GUI window and open a message loop. The main difference is that a console program is auto-attached to the console of the process that launched it, if any.
I'm not certain what the rules are for what the C library does, but it seems to detect the no-console case on startup and routes stderr to NUL. So printf and friends still do nothing even after allocating a console. I think you can fix around this with something like this, having allocated a console:
fclose(stdout);stdout=freopen("CON","w",stdout);
fclose(stderr);stderr=freopen("CON","w",stderr);
You can probably actually arrange for stdout and stderr to go to STD_OUTPUT_HANDLE and STD_ERROR_HANDLE, if the distinction would matter, but that'd be a bit more hassle (see https://learn.microsoft.com/en-us/cpp/c-runtime-library/refe...).pjc50
The canonical answer seems to be https://devblogs.microsoft.com/oldnewthing/20090101-00/?p=19... : there's a flag in the PE executable header. But as you say, the same APIs are available to both once you're running. So you can have a console application that decides it's going to open some windows.
o11c
Isn't the main practical problem also that a console program will forcibly create a console if it doesn't already have one? This is what breaks the approach everyone else uses of "write to stderr under the assumption it will be visible only if the user was prepared for it, without causing clutter otherwise".
abnercoimbre
This is information that I needed for my own work. Thank you for taking the time to share knowledge.
DSMan195276
Yes, but after the program is started you can do anything. AFAIK the important part of the distinction is the behavior when starting the program - if you start a GUI program from the console (Ex. calc), cmd doesn't wait for it to exit. And if you start a console program from Explorer, a cmd window will pop-up (you can hide it, but it will still flicker briefly).
delfinom
WM_TIMER has a flaw the author is not aware of. It is a low priority event that yields to pretty much all other events in the message loop.
Meaning if a user is busy jerking off a mouse which is causing WM_MOUSEMOVE events, the WM_TIMER will never fire at the expected time.
There is a hack to peek the next event specifically looking for WM_TIMER, on every other event call, which would trigger it on the queue.
https://devblogs.microsoft.com/oldnewthing/20191108-00/?p=10...
gpderetta
I find that you already have an event loop built on top of poll/epoll/io_uring, maintaining a timer heap (or wheel) in userspace and setting the polling timeout to the next expiration is the easiest and more efficient solution. Use an eventfd to force an early wakeup if you modify timers concurrently with your waits.
The most annoying thing is that epoll supports timeouts smaller than a millisecond only since 5.11.
edit: that's literally what's described under "All OSes: timers fully implemented in userspace ", including the comment about epoll low resolution.
mrpippy
macOS does support EVFILT_TIMER, unfortunately the man pages that Apple hosts are very out-of-date. A more recent page (https://www.manpagez.com/man/2/kqueue/osx-10.13.1.php) shows support, and it's been there since at least 10.9 (2013).
elcritch
Awesome, that’s a handy link. I made a PR to Nim’s standard libraries async to use that (or a similar flag) MacOS. It massively improved latency, great for UIs.
I created an issue for another async system showing them how to do it. They closed the issue saying “MacOS doesn’t support that”. I reckon people don’t know how to actually research these things if it’s not 1-click away.
devit
The article's conclusion is completely wrong because it misses two crucial points:
1. On multicore machines you want to process timers in parallel on multiple cores. With userspace timers you either set the same timeouts on all threads and have unnecessary wakeups or distribute timers to cores ahead of time which leads to increased latency if a thread is stalled for any reason. I think this is unfixable without a dedicated timer API.
2. Good timer APIs let you set a time _interval_ for when the timer expires, which is essential so that the system can group timers and reduce wakeups (i.e. you process all timers where the lower bound has been reached before going to sleep, but don't wake up until the upper bound arrives). Most or all "wait with timeout" APIs only have a single timeout, although this could be fixed.
gpderetta
> On multicore machines you want to process timers in parallel on multiple cores.
By experience I almost never want to run my timer callbacks on a random core and always want it on a specific core. With the typical one event loop per thread, you would register the timer with the thread you care about.
This is going to be very application specific.
thijsvandien
Windows offers several kinds of timers. For example, there's also CreateTimerQueueTimer, which (in contrast to SetTimer) does not require a message loop.
Veliladon
I miss having an NMI that you got at 50/60Hz come hell or high water.
mrguyorama
This is more related to the fact that computing systems are usually preemptive multitasking systems that are expected to operate very predictably even with stuff happening in the "background".
If you really want or need reliable timers or miss those days for any reason, check out a Real Time OS.
o11c
That's basically what `timer_settime` does if you have a realtime scheduler.
touisteur
People sleep on /dev/rtc and PIE !
pjc50
.. or the MSDOS/PC timer interrupt at something like 15Hz?
https://www.xtof.info/Timing-on-PC-familly-under-DOS.html (wow, this is more complicated than I remember)
This article fails to separate the concerns one has with timers. There's three cases I see:
The simple case is just putting a thread to sleep for a given time. It's somewhat odd that there's no portable API for that, granted. At least POSIX has sleep, nanosleep and clock_nanosleep (list edited, thanks oguz-ismail).
The second case is that a thread wants to continue processing until a timer has passed. This has to happen cooperatively one way or another, so setting a timer is equivalent to regularly querying the system time and comparing to a limit. There is simply no need for a "timer" operating system facility. (Even if you were to plug POSIX timers in here, you'd in all likelihood still need to check some flag set in the signal handler in the processing thread. And then you'd still need to check the system time as signals can originate from anywhere, just as with sleep/etc. above.)
In the third case, a thread waits for I/O. Then, of course, the call to that I/O facility defines how any timer is handled. As these facilities are not portable, timers aren't portable. The author refers to this realization as the need to implement "Userspace timers". These are just an artifact of the I/O facility and calls querying the system time, though.
So, ultimately, not having portable timers is the least of our problems. So long as we don't unify select, poll, kqueue, io_uring and what not, we don't have a need for them. And, as the author realized, libraries like libuv, which do an acceptable job at unifying those facilities, tend to provide a portable timer API.