1. 28
  1. 20

    Reminds me of a sign we used to have in the Mozilla San Francisco office, hanging about 2.2m high and reading “you must be this tall to write multi-threaded code”.

    Corollary, it should be somewhat easier to synchronize and do book-keeping for Signal handling with send/sync semantics in e.g., Rust, no?

    1. 11

      Not in my experience. Unix signals are so mucky and full of sharp edges that they can’t really exist within Rust’s safety model, as far as I can tell. The best Rust can do is make it a bit easier to do all the painfully exact accounting that you have to do with the raw API anyway.

      Basically your multi-threaded app needs a safe shutdown command that can be entered at any time, which Rust does make easier but is still kind of a pain. But, for example, killing a thread from the outside isn’t guaranteed to run destructors. So if you have a long running processing thread, your choices are “wait for it to finish” or “kill it rudely and the OS will clean it up when the process exits”. It’s a nasty situation.

      1. 6

        Async rust makes it theoretically possible to kill any task (and you can just have one task per thread if you want), including running destructors, at any await point. Normal rust programs don’t take advantage of this, but I don’t think making one that does would be hard.

        Obviously the extra book-keeping inserted by the compiler would come at a performance cost.

    2. 16

      This article was long enough they could have packed in some links to educational content for how to learn these dark arts.

      I recently ran into this program again while working on a daemon that needed to clean up it’s unix socket file when terminating. I used to handle this fairly well in my single-threaded C Linux programs back in the day (circa 1995-2005) just by adjusting the signal handlers. My recent experience echos the article though. In the intervening years the ubiquity of threads has changed the game. In today’s open source ecosystem where dependencies could spin up threads without you knowing it is particularly difficult.

      For my project I found the ctrlc rust crate. My first read through the code was “what is all of this garbage?” but as I re-read it I realized that (ignoring the cross-platform stuff) it is implementing the self-pipe pattern, which does indeed seem to be a reliable and minimal solution to the problem (and other related problems).

      1. 12

        Getting ctrl-c right is usually not a simple task, but I really dislike the defeatist schtick here. The requirements posited by the author for ctrl-c are ideals, not hard and fast laws. In a better world we would not have signals and we’d have a way to magically suspend computations at any point.

        However, we don’t have that, and we make do with what we have. This is the type of thing that gets easier with experience and hindsight. Writing it off as “for wizards” is not great.

        1. 6

          I would like to see some tips on how to implement a SIGINT handler. My understanding has been that you can’t do anything safely other than set a flag, so that’s what I’ve done the few times I’ve implemented one.

          1. 10

            Yeah that’s all you can do in the handler. The rest of the handling logic is spread throughout the rest of the program as you try to handle, everywhere, the case, everywhere, that the user wants to exit.

            Imagine you are waiting on network IO to some SQL database when you receive SIGINT/TERM. What now? Do you cut what you are doing or wait a bit longer? If so, how long?

            Someone else mentioned the unintentional “Real Programmers” elements to this article and it certainly smacked of that in places. Especially the bit about invariably wrapping up what you’re doing inside of 30ms.

            1. 3

              You can do anything in a signal handler except call signal-unsafe functions. In general, you should assume that any function is signal unsafe if it might (now or in the future):

              • Acquire locks (including indirectly, for example via memory allocation), or
              • Modify any thread-local state.

              The key problem with signals is that they can top any thread in your program at any point in between two instructions, which might be in the middle of a C statement. If you attempt to acquire a lock, you may find that the thread that you’ve interrupted holds the lock and now you have a thread deadlocking with itself. This is particularly dangerous with recursive locks, where the second acquisition will succeed, but now you’ve got concurrent mutation of the thing that the lock was protecting. Modifying thread-local state is also problematic because most functions assume that thread-local state can’t be mutated out from under them, yet this can happen in signals.

              As a rule of thumb, the following things are safe:

              • Modifying individual words of memory, especially _Atomic words (but be very careful about larger _Atomic types because they might be lowered to a call with locks.
              • Calling system calls

              This means that you can set a flag or you can wake up a pipe. You need to be careful of things like printf because they might acquire locks to load locales. That kind of thing will work 99.99% of the time, so it’s fine in a debug build that you never share with anyone else but is problematic if it makes it into CI (for example).

              When I have to use Linux, the first thing I notice is that ^T doesn’t send SIGINFO. Most *BSD (including Mac) long-running utilities will print status information to standard error on SIGINFO, so you can see what’s happening in a long-running task.

            2. 5

              Maybe shells could have a sudo-ish version of Ctrl-c that upgrades it to SIGKILL “just this one time”.

              1. 5

                CTRL-\ is close.

                1. 4

                  I think I did that - my shell sends the (Windows equivalent of a) signal to the child process, starts unwinding to get back to the prompt, and if the process is still there in 50ms it gets terminated. That’s not quite what the article author wants - he seems to want programs to be able to do things other than termination in response to the signal.

                2. 5

                  It may be worth noting here that keeping Ctrl-C functional is quite tractable in Haskell thanks to its async exception support and its structured parallelism/concurrency utilities. In a Haskell program, the default SIGINT handler will just throw an async exception (just like the ones you can throw to a running thread) to one of the running (lightweight) threads and as long as you follow some established best practices not to swallow async exceptions and use the standard structured concurrency utilities that propagate exceptions to your parent thread, your program will exit gracefully collecting all the resources and print any shutdown messages etc.

                  In my experience, all the Haskell programs I’ve interacted with seemed to follow these best practices since they handle Ctrl-C gracefully.

                  1. 1

                    your program will exit gracefully collecting all the resources and print any shutdown messages etc.

                    The article is not advocating for Ctrl+C to shut down the program, it’s advocating for Ctrl+C to pause/reset the still-running program and allow execution to continue from that point, with the program’s running state at the moment of the Ctrl+C preserved as much as possible.

                    1. 2

                      Thats not what SIGTERM is for though, for that there is SIGSTOP/SIGCONT. If the goal is to use SIGTERM wrong I’m not sure this article is worth much.

                      1. 1

                        Ctrl+C sends SIGINT.

                        1. 1

                          Ah yeah brainfart for some reason I was thinking TERM. Still don’t see a point to it compared to SIGSTOP/CONT though, used those for years to pause an executable and resume it later (notably firefox) to save battery.