4

Thread-safe or thread-compatible code is good. However there are cases in which one could implement things differently (more simply or more efficiently) if one knows that the program will not be using threads.

For example, I once heard that things like std::shared_ptr could use different implementations to optimize the non-threaded case (but I can't find a reference). I think historically std::string in some implementation could use Copy-on-write in non-threaded code.

I am not in favor or against these techniques but I would like to know if that there is a way, (at least a nominal way) to determine at compile time if the code is being compiled with the intention of using threads.

The closest I could get is to realize that threaded code is usually (?) compiled with the -pthreads (not -lpthreads) compiler option. (Not sure if it is a hard requirement or just recommended.)

In turn -pthreads defines some macros, like _REENTRANT or _THREAD_SAFE, at least in gcc and clang. In some some answers in SO, I also read that they are obsolete.

Are these macros the right way to determine if the program is intended to be used with threads? (e.g. threads launched from that same program). Are there other mechanism to detect this at compile time? How confident would the detection method be?


EDIT: since the question can be applied to many contexts apparently, let me give a concrete case:

I am writing a header only library that uses another 3rd party library inside. I would like to know if I should initialize that library to be thread-safe (or at least give a certain level of thread support). If I assume the maximum level of thread support but the user of the library will not be using threads then there will be cost paid for nothing. Since the 3rd library is an implementation detail I though I could make a decision about the level of thread safety requested based on a guess.


EDIT2 (2021): By chance I found this historical (but influential) library Blitz++ which in the documentation says (emphasis mine)

8.1 Blitz++ and thread safety

To enable thread-safety in Blitz++, you need to do one of these things:

  • Compile with gcc -pthread, or CC -mt under Solaris. (These options define_REENTRANT,which tells Blitz++ to generate thread-safe code).
  • Compile with -DBZ_THREADSAFE, or #define BZ_THREADSAFE before including any Blitz++ headers.

In threadsafe mode, Blitz++ array reference counts are safeguarded by a mutex. By default, pthread mutexes are used. If you would prefer a different mutex implementation, add the appropriate BZ_MUTEX macros to <blitz/blitz.h> and send them toblitz-dev@oonumerics.org for incorporation. Blitz++ does not do locking for every array element access; this would result in terrible performance. It is the job of the library user to ensure that appropriate synchronization is used.

So it seems that at some point _REENTRANT was used as a clue for the need of multi-threading code. Maybe it is a very old reference to take seriously.

alfC
  • 14,261
  • 4
  • 67
  • 118
  • I don't think the compiler does something different if multiple threads are not being used. This doesn't sound right to me. Hoping some expert to shed light on it! – abhiarora May 25 '20 at 09:30
  • 1
    The intent is to have a static library that provides a not thread safe implementation when a program is compiled without thread support? – Oliv May 25 '20 at 09:36
  • 1
    @Oliv, that can be a case. Although I was thinking a more simple case, I have a header only library that uses another 3rd party library inside. I would like to know if I should initialize that library to be thread-safe (or at least give a certain level of thread support). If I assume the maximum level of thread support but the user of the library will not be using threads then there will be cost paid for nothing. Since the 3rd library is an implementation detail I though I could make a decision about the level of thread safety requested. – alfC May 25 '20 at 09:40
  • 1
    There is a case I don't see how it could solved. Suppose the client compiles a dynamic library without thread support. Your library notice that and the 3rd party library is initialized without thread support. What happens when this library is linked to a multithread program? There are maybe things that can be done with ld... – Oliv May 25 '20 at 10:27
  • 1
    I'm not sure that there is a reliable way to determine if code is compiled with the intent of being used in a multithreaded process or not. POSIX threads were [originally an optional extension](https://standards.ieee.org/standard/1003_1c-1995.html), but they are now part of the core POSIX standard. [`pthread.h`](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/pthread.h.html#tag_13_36) shows only a few optional features. It's probably even harder now that threading is part of the C and C++ standards. – Andrew Henle May 25 '20 at 20:14
  • @AndrewHenle So, I guess a (header) library has its own macro to indicate if the library is going to be used with(in) threads o not. – alfC May 25 '20 at 20:16
  • 1
    I do this the old-fashioned way --- if a single-threaded program wants to use my library and doesn't want to pay the cost of thread-safety, the program can simply recompile the library with e.g. `-DTHIS_PROGRAM_IS_SINGLE_THREADED_I_PROMISE` as one of the command-line arguments, and then link to that build of the library. That way the default build of the library is thread-safe and will work everywhere, but if someone cares enough to go read the build-docs and go out of their way to get that last bit of efficiency in their single-threaded program, they can. – Jeremy Friesner Mar 15 '21 at 03:20

2 Answers2

2

How confident would the detection method be?

Not really. Even if you can unambiguously detect if code is compiled to be used with multiple threads, not everything must be thread safe.

Making everything thread-safe by default, even though it is only ever used only by a single thread would defeat the purpose of your approach. You need more fine grainded control to turn on/off thread safety if you do not want to pay for what you do not use.

If you have class that has a thread-safe and a non-thread-safe version then you could use a template parameter

class <bool isThreadSafe> Foo;

and let the user decide on a case for case basis.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
2

I support the other answer in that thread-safety decision ideally should not be done on whole program basis, rather they should be for specific areas.

Note that boost::shared_ptr has thread-unsafe version called boost::local_shared_ptr. boost::intrusive_ptr has safe and unsafe counter implementation.

Some libraries use "null mutex" pattern, that is a mutex, which does nothing on lock / unlock. See boost or Intel TBB null_mutex, or ATL CComFakeCriticalSection. This is specifically to substitute real mutex for threqad-safe code, and a fake one for thread-unsafe.

Even more, sometimes it may make sense to use the same objects in thread-safe and thread-unsafe way, depending on current phase of execution. There's also atomic_ref which serves the purpose of providing thread-safe access to underlying type, but still letting work with it in thread unsafe.

I know a good example of runtime switches between thread-safe and thread-unsafe. See HeapCreate with HEAP_NO_SERIALIZE, and HeapAlloc with HEAP_NO_SERIALIZE.

I know also a questionable example of the same. Delphi recommends calling its BeginThread wrapper instead of CreateThread API function. The wrapper sets a global variable telling that from now on Delphi Memory Manager should be thread-safe. Not sure if this behavior is still in place, but it was there for Delphi 7.


Fun fact: in Windows 10, there are virtually no single-threaded programs. Before the first statement in main is executed, static DLL dependencies are loaded. Current Windows version makes this DLL loading paralleled where possible by using thread pool. Once program is loaded, thread pool threads are waiting for other tasks that could be issued by using of Windows API calls or std::async. Sure if program by itself will not use threads and TLS, it will not notice, but technically it is multi-threaded from the OS perspective.

Alex Guteniev
  • 12,039
  • 2
  • 34
  • 79
  • Thank you for your experienced answer. Is `(local_)shared_ptr` a good example to follow? Completely separate types? Are there example of "runtime" thread-safety in which the choice is made at runtime? I guess the runtime configuration will have to be thread safe in itself. – alfC May 27 '20 at 07:34
  • `HeapCreate` with `HEAP_NO_SERIALIZE` is a good example of runtime configuration. Delphi `BeginThread` setting a global variable for memory manager is a bad example of runtime configuration. – Alex Guteniev May 27 '20 at 07:44
  • 1
    `local_shared_ptr` is sort of compromise solution. Smart pointers could have been flexibly configured using Alexandrescu-style policies. The goal of `shared_ptr` desing was like "we want type erasure, we don't want policies". But thread safety overhead turned out to be too much significant, also there are reasons for `shared_ptr` that runtime configuration won't be a good idea. So, for `shared_ptr` probably making `local_` counterpart is good. – Alex Guteniev May 27 '20 at 07:51