3

1) I'm new to std::thread and I would like to know whether it is a good practice to call pthread_sigmask() to block some signals in a particular thread created by std::thread.

I don't want the new thread to receive signals such as SIGTERM, SIGHUP, etc., because the main process has already installed handlers for these signals.

So, is it a good practice to call pthread_sigmask() to block some signals in a thread created by std::thread?

2) Also, I believe the effect of pthread_sigmask(SIG_BLOCK, &mask, NULL) will only apply to the thread created using

std::thread(&Log::rotate_log, this, _logfile, _max_files, _compress).detach();

and calling rotate_log() as the start function.

And the effect of pthread_sigmask(SIG_BLOCK, &mask, NULL) will not apply to the thread where std::thread(&Log::rotate_log, this, _logfile, _max_files, _compress).detach() is called.

Is my understanding correct?

void rotate_log (std::string logfile, uint32_t max_files, bool compress)
{
    sigset_t mask;

    sigemptyset (&mask);
    sigaddset (&mask, SIGTERM);
    sigaddset (&mask, SIGHUP);
    pthread_sigmask(SIG_BLOCK, &mask, NULL);

    // Do other stuff.
}

void Log::log (std::string message)
    {
        // Lock using mutex
        std::lock_guard<std::mutex> lck(mtx);

        _outputFile << message << std::endl;
        _outputFile.flush();
        _sequence_number++;
        _curr_file_size = _outputFile.tellp();

        if (_curr_file_size >= max_size) {
            // Code to close the file stream, rename the file, and reopen
            ...


            // Create an independent thread to compress the file since
            // it takes some time to compress huge files.
            if (!_log_compression_on)
            {
                std::thread(&Log::rotate_log, this, _logfile, _max_files, _compress).detach();
            }
        }
    }
void
  • 338
  • 5
  • 19
  • That would at the very least assume that std::thread is implemented in terms of pthreads, which may not be the case. –  Feb 25 '19 at 16:56
  • I have already mentioned the purpose in my question as "I don't want the new thread to receive signals such as SIGTERM, SIGHUP, etc., because the main process has already installed handlers for these signals." Now, in that context, I would like to know whether the above example serves that purpose and whether what I am doing is a good practice or not. – void Feb 25 '19 at 17:02

3 Answers3

7

In theory, it is possible that an implementation of std::thread will create a non-POSIX thread even on a system which has POSIX threads, and pthread_sigmask would not work for such threads. (Maxim Egorushkin's comment is correct, though—you really should block signals in the thread-creating thread and only unblock those you want handled on the new thread, to avoid race conditions.)

I cannot speak for other implementations, but it it is extremely unlikely that such a thing will happen for the GNU/Linux implementation. I cannot speak authoritatively for this implementation either, of course, but there is simply too much C and C++ code out there that assumes a 1:1 mapping between userspace threads (whether C, C++, or POSIX) and kernel tasks (those things that have TIDs). Ten years ago, people still argued that an n:m threading library would be a possibility (of which the early 1:m implementations of POSIX threads were just a special case).

But today, programmers call unshare (CLONE_FS) from a thread to give that thread a private current directory, separate from all the other threads. They call setfscreatecon and expect that this only affects the calling thread. They even call the system calls setresuid and setresgid directly because they want to avoid the setxid broadcast that glibc uses to propagate the change to all threads (something that the kernel does not support directly). All this would stop working under an n:m threading model. So std::thread and POSIX threads would have to map to kernel tasks, enforcing the 1:1 model.

In addition, there is just one GNU TLS ABI for both C and C++, and that in turn pretty much requires that there can only be one type of thread in the system, with one thread pointer that is used to eventually reach thread-local data.

That's why it's so unlikely that on GNU/Linux, std::thread will ever use anything but the POSIX threads provided by glibc.

VZ.
  • 21,740
  • 3
  • 39
  • 42
Florian Weimer
  • 32,022
  • 3
  • 48
  • 92
  • 2
    On Linux one can also use [signalfd](http://man7.org/linux/man-pages/man2/signalfd.2.html) for a *much* nicer way of dealing with signals. – Jesper Juhl Feb 25 '19 at 18:29
5

The correct way is to set the required signal mask in the parent before creating a thread and then revert it in the parent. This way your newly created thread has the correct signal mask set from its very start. (The signal mask is inherited from the parent thread).

When you set the signal mask after the thread has started, there is a window of time during which the thread doesn't have the required mask.

Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
  • But what happens if I need to spawn a new thread LATER on-the-fly and a signal occurs BEFORE the mask (blocks all signals) has been reverted? Don't I then lose the signal completely? What is the best practice here regarding std::threads, if I ONLY want to get signals/handle signals in the main thread? (What about std::async?) – SoulfreezerXP May 16 '23 at 17:21
  • 1
    @SoulfreezerXP Blocked signals don't get lots, see https://man7.org/linux/man-pages/man7/signal.7.html. Signal handling good practice https://stackoverflow.com/a/6954532/412080 – Maxim Egorushkin May 16 '23 at 18:23
  • Very interesting! That means, in my scenario a blocked signal will stay in some pending-state, until the parent has reverted its mask? – SoulfreezerXP May 16 '23 at 20:36
  • 1
    @SoulfreezerXP Blocked signals stay pending and don't get lost. When the signal is unblocked, pending signals get delivered to the signal handler, which can be set to `SIG_IGN` to ignore the signal. In other words, you cannot miss a signal accidentally. That man page explains it in full detail and is the authoritative source, unlike me. – Maxim Egorushkin May 17 '23 at 14:25
  • One last important question please. If I set the mask in the parent (all blocked) and DIRECTLY AFTER pthread_create(...) (e.g. via RAII) I restore the mask in the parent, is it GUARANTEED that the new thread got the 1st version of the mask (all blocked) as a copy, or can there be a race condition too? Does pthread_create(...) create its thread asynchronously? What is the best practice here (also considering std::thread)? – SoulfreezerXP May 22 '23 at 11:33
  • @SoulfreezerXP IMO, the more important question is what makes you think otherwise? – Maxim Egorushkin May 22 '23 at 12:01
  • I can't recreate it, but it must be ensured that something like this can't happen in production. The scenario described has the potential for a classic race condition. I have opened a new question on this. https://stackoverflow.com/questions/76306100/is-calling-pthread-sigmask-before-creating-a-thread-thread-safe – SoulfreezerXP May 22 '23 at 12:16
  • @SoulfreezerXP IMO, https://man7.org/linux/man-pages/man7/signal.7.html contains answers to your questions. – Maxim Egorushkin May 22 '23 at 12:35
  • I cannot find the answers there nor at https://man7.org/linux/man-pages/man3/pthread_create.3.html Maybe you can point me in the right direction? – SoulfreezerXP May 22 '23 at 13:01
  • @SoulfreezerXP https://man7.org/linux/man-pages/man7/signal.7.html: _Each thread in a process has an independent signal mask, which indicates the set of signals that the thread is currently blocking. A thread can manipulate its signal mask using pthread_sigmask(3). In a traditional single-threaded application, sigprocmask(2) can be used to manipulate the signal mask. A child created via fork(2) inherits a copy of its parent's signal mask; the signal mask is preserved across execve(2)_. – Maxim Egorushkin May 22 '23 at 13:09
  • @SoulfreezerXP https://man7.org/linux/man-pages/man3/pthread_create.3.html: _The new thread inherits a copy of the creating thread's signal mask (pthread_sigmask(3)). The set of pending signals for the new thread is empty (sigpending(2)). The new thread does not inherit the creating thread's alternate signal stack (sigaltstack(2))._ – Maxim Egorushkin May 22 '23 at 13:09
  • First, thnx for your effort! Yes I have read this, but nowhere is it explained when exactly the mask is copied and if pthread_create blocks until the mask is copied. I tried to understand the source code of pthread_create but it is hard to read. It would be great if you could try to answer my question from the link above if you feel like it. This could be helpful for many other people too! – SoulfreezerXP May 22 '23 at 13:18
  • @SoulfreezerXP When `pthread_create` returns all its required effects have taken place. The new thread inherits the parent thread's signal mask that was in effect before `pthread_create` call. What happens to the parent's signal mask after `pthread_create` returns has no effect on any other threads, even if they didn't start executing yet. Your worries aren't grounded in fact or logic. – Maxim Egorushkin May 22 '23 at 13:25
  • I would very much like to sign it immediately, but I was hoping for a source to back it up. Then I have to "trust" the thing, which is usually a suboptimal way. Nevertheless, thank you very much for your effort! – SoulfreezerXP May 22 '23 at 13:48
  • @SoulfreezerXP You essentially make a claim that changing one thread's signal mask affects signal masks of other threads and/or that the signal mask inheritance/copying in `pthread_create` suffers from race conditions. You do not provide any evidence or basis for your strong claim but demand evidence or quote from a standard of why your hypothetical catastrophe scenario cannot possibly happen. In scientific world, that which is claimed without evidence, can be dismissed without evidence. – Maxim Egorushkin May 22 '23 at 16:09
  • I only wanted to make sure, that here no "late init" could happen in the new thread to make pthread_create returning faster. pthread_create sounds for me as such a candidate, because it is creating AND starting a thread. I thought it could be legal to weight some init code to the asynchronous side and declare it as "start time" of the thread. – SoulfreezerXP May 22 '23 at 21:23
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/253778/discussion-between-soulfreezerxp-and-maxim-egorushkin). – SoulfreezerXP May 23 '23 at 05:24
  • @SoulfreezerXP @SoulfreezerXP When `pthread_create` returns, the new thread has been created and all the side effects required of `pthread_create` have taken place. When that thread actually gets to run on a CPU is an orthogonal matter outside of `pthread_create` scope. There are no such things as "late init" or "start time" for `pthread_create`. If you claim otherwise, that would be violation of requirements for `pthread_create`, a bug to report against your PThreads implementation with code reproduction. – Maxim Egorushkin May 23 '23 at 18:43
  • @SoulfreezerXP The standard requirement for APIs is to be atomic and strong-exception-safe, any diversion from that must be documented in large flashing red letters. That is, when a function succeeds, all its side effects have taken place; another thread can only observe none or all of the side effects, never a partially updated state. When a function fails, it doesn't change the state of the system. – Maxim Egorushkin May 23 '23 at 18:59
  • 1
    Yes I agree and I have also finally found the relevant parts in the glibc source code that give me the certainty I was looking for. Thank you and I have no more questions about this topic. – SoulfreezerXP May 23 '23 at 21:08
  • @SoulfreezerXP In Linux there is [`clone`](https://man7.org/linux/man-pages/man2/clone.2.html) family of functions which create new threads in the same or new address space, `pthread_create`, `fork`, `posix_spawn`, etc., delegate to those. That's where the signal mask inheritance happen atomically, amongst other things. – Maxim Egorushkin May 23 '23 at 22:03
1

If your need to set the signal mask in a multithreaded program on a POSIX system, then pthread_sigmask is the function that you need to use. There are no functions to interact with signal masks in the C++ standard library.

Also, I believe the effect of pthread_sigmask(SIG_BLOCK, &mask, NULL) will only apply to the thread created using ...

pthread_sigmask applies to the thread in which the call was executed, regardless of how the thread has been created. It applies to no other pre-existing threads. In the case of your example, the function rotate_log in which pthread_sigmask is called is executed in the thread created by std::thread.

eerorika
  • 232,697
  • 12
  • 197
  • 326