3

Let's assume that I have this function:

int my_thread_id(){
  static int counter {0};
  thread_local int tid{++counter};
  return tid;
}

Is this function (my_thread_id) async-signal-safe, even on the first call?

ildjarn
  • 62,044
  • 9
  • 127
  • 211
Mae Milano
  • 714
  • 4
  • 14

2 Answers2

5

This answer can be considered an addendum to the answer given by ildjarn. When strictly speaking about the C++ 14 standard, using data with thread_local storage from a signal handler results in undefined behavior.

On certain platforms, however, such use might be permissible. For example, most POSIX systems implement thread-local storage by using a special data segment that is allocated per-thread (like the stack). Please refer to this document for detailed explanations. In this case, access to thread-local data is async signal safe because it does not involve any locks.

However, the data read or written by the signal handler might still be inconsistent unless only atomics are accessed or access is fenced by using std::atomic_signal_fence. The reason for this is that the compiler has no idea when a signal handler might interrupt execution and might thus reorder read and write instructions. std::atomic_signal_fence prohibits this reordering and reordering by the CPU is not a problem because the execution happens within the same thread and the CPU is only allowed to reorder instructions when the result (within the thread) is the same as if the instructions had been executed in order.

In addition to std::atomic_signal_fence, using variables of type std::atomic is safe, as long they are lock free (as indicated by std::is_lock_free).

On Linux (and I believe most other POSIX platforms), the question whether a signal is dispatched to a specific thread depends on how this signal is generated and the exact type of the signal. For example, SIGSEGV and SIGBUS are always dispatched to the thread that caused the error resulting in the signal. In this case, using thread-local storage can be a convenient way to recover from such errors. However, there is no way to do this while keeping the code portable to all platforms supporting the C++ standard.

Petr Skocik
  • 58,047
  • 6
  • 95
  • 142
  • 1
    I believe this is not entirely correct. The document mentioned in this answer specifies that the address of the TLS data might be determined by calling __tls_get_addr(), and that function might call malloc() if the memory is not set up yet - hence usage of a lock. Not entirely sure as to the conditions under which the compiler emits calls to __tls_get_addr(), and the conditions under which __tls_get_addr() calls malloc() but I did see it happen (with a loaded plugin). Probably the workaround is to ensure the TLS data is accessed before the signal fires - this prevents the malloc() case. – Uri Simchoni Sep 13 '18 at 19:38
3

No.

Signal handlers have no notion of which thread they're executing on, so thread_local has no valid semantics there. [intro.multithread]p2:

A signal handler that is executed as a result of a call to the raise function belongs to the same thread of execution as the call to the raise function. Otherwise it is unspecified which thread of execution contains a signal handler invocation.

Also relevant is p23:

Two actions are potentially concurrent if

  • they are performed by different threads, or
  • they are unsequenced, and at least one is performed by a signal handler.

The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior.

(The special case for signal handlers being referred to is only regarding the type volatile sig_atomic_t and does not apply here.)

The second bullet pertains because of [intro.execution]p6:

If a signal handler is executed as a result of a call to the raise function, then the execution of the handler is sequenced after the invocation of the raise function and before its return. [ Note: When a signal is received for another reason, the execution of the signal handler is usually unsequenced with respect to the rest of the program. —end note ]

ildjarn
  • 62,044
  • 9
  • 127
  • 211
  • 1
    Signal handlers are constrained by [\[support.runtime\]/10](http://eel.is/c++draft/support.runtime#10). – T.C. Apr 14 '16 at 07:16