[HN Gopher] The missing cross-platform OS API for timers
       ___________________________________________________________________
        
       The missing cross-platform OS API for timers
        
       Author : broken_broken_
       Score  : 43 points
       Date   : 2025-02-03 06:07 UTC (3 days ago)
        
 (HTM) web link (gaultier.github.io)
 (TXT) w3m dump (gaultier.github.io)
        
       | pjc50 wrote:
       | 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 wrote:
         | 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_ wrote:
           | 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 wrote:
             | 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.
        
             | abnercoimbre wrote:
             | This is information that I needed for my own work. Thank
             | you for taking the time to share knowledge.
        
             | o11c wrote:
             | 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".
        
           | DSMan195276 wrote:
           | 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).
        
       | hkwerf wrote:
       | 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.
        
         | Someone wrote:
         | > 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 wrote:
           | > 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 wrote:
             | 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.
        
         | oguz-ismail wrote:
         | >usleep
         | 
         | Not since 2008. POSIX has sleep(), nanosleep() and
         | clock_nanosleep() now
        
           | hkwerf wrote:
           | Thanks, I've edited the list. :)
        
         | amelius wrote:
         | 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 wrote:
           | 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
        
         | robertlagrant wrote:
         | Which concerns were not separated?
        
         | russdill wrote:
         | 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?
        
       | gpderetta wrote:
       | 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.
        
       | devit wrote:
       | 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 wrote:
         | > 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.
        
       | Veliladon wrote:
       | I miss having an NMI that you got at 50/60Hz come hell or high
       | water.
        
         | pjc50 wrote:
         | .. 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)
        
         | touisteur wrote:
         | People sleep on /dev/rtc and PIE !
        
         | o11c wrote:
         | That's basically what `timer_settime` does if you have a
         | realtime scheduler.
        
         | mrguyorama wrote:
         | 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.
        
       | thijsvandien wrote:
       | Windows offers several kinds of timers. For example, there's also
       | _CreateTimerQueueTimer_ , which (in contrast to _SetTimer_ ) does
       | not require a message loop.
        
       | tom_ wrote:
       | 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 wrote:
         | 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.
        
       | mrpippy wrote:
       | 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).
        
       ___________________________________________________________________
       (page generated 2025-02-06 23:01 UTC)