Remix.run Logo
asveikau 7 months ago

I think doing those timeouts transparently would be tricky under the constraints of POSIX and ISO C. It would need to have some cooperation from the application layer

jart 7 months ago | parent [-]

The only way you'd be able to do it is by having functions like fputc() call clock_gettime(CLOCK_MONOTONIC_COARSE) which will impose ~3ns overhead on platforms like x86-64 Linux which have a vDSO implementation. So it can be practical sort of although it'd probably be smarter to just use line buffered or unbuffered stdio. In practice even unbuffered i/o isn't that bad. It's the default for stderr. It actually is buffered in practice, since even in unbuffered mode, functions like printf() still buffer internally. You just get assurances whatever it prints will be flushed by the end of the call.

asveikau 7 months ago | parent [-]

That's just for checking the clock. You'd also need to have a way of getting called back when the timeout expires, after fputc et al are long gone from the stack and your program is busy somewhere else, or maybe blocked.

Timeouts are usually done with signals (a safety nightmare, so no thanks) or an event loop. Hence my thought that you can't do it really transparently while keeping current interfaces.

jart 7 months ago | parent [-]

Signals aren't a nightmare it's just that fflush() isn't defined by POSIX as being asynchronous signal safe. You could change all your stdio functions to block signals while running, but then you'd be adding like two system calls to every fputc() call. Smart thing to do would probably be creating a thread with a for (;;) { usleep(10000); fflush(stdout); } loop.

asveikau 7 months ago | parent [-]

Signals are indeed a nightmare. Your example of adding tons of syscalls to make up for lack of safety shows that you understand that to be true.

And no, creating threads to solve this fringe problem in a spin loop with a sleep is not what I'd call "smart". It's unnecessary complexity and in most cases, totally wasted work.

jart 7 months ago | parent [-]

The smartest thing to do is still probably not buffering. What's wrong with the thread? It would take maybe 15 lines of code to implement. It would be correct without rarely occurring bugs. It doesn't need signals or timers. It wouldn't add overhead to stdio calls. It's a generalized abstraction. You won't need to change your program's event loop code. Create the thread with a tiny 64kb stack and what's not to like? Granted, it would rub me the wrong way if libc did this by default, since I wouldn't want mystery threads appearing in htop for my hello world programs. But for an app developer, this is a sure fire solution.

klempner 7 months ago | parent | next [-]

How exactly does this interact with fork()?

jart 7 months ago | parent [-]

Your libc fork() implementation will lock the stdio mutexes automatically before calling SYS_fork, so the code I wrote in my comment would be fork safe. Your child process would need to spawn the flusher thread again if it's desired though.

asveikau 7 months ago | parent | prev [-]

> It would take maybe 15 lines of code to implement

This is a very bad reason to justify something. Especially introduce threads. Your response here is like saying "I don't know why people say it's so hard to write multi threaded programs, the thread create API is so simple." It completely misses the point why added complexity can be harmful.

> without rarely occurring bugs.

Except for a glaring thing like "what if fflush gets an I/O error in this background thread"?

> Granted, it would rub me the wrong way if libc did this by default,

This is exactly my point. It needs cooperation from the application layer. It wouldn't make sense to be transparent.

jart 7 months ago | parent [-]

Almost no one checks for error when printing to stdout. That's why SIGPIPE exists.

Complexity, readability, etc. is the argument people make when they've run out of arguments.