What is thread-safe initialization of local static objects - Page 1
Forum section: electronics / general

author: peter-h
author_rank: Super Contributor
timestamp: August 04, 2023, 01:15:51 pm
content: With Cube IDE v 1.13.1 I get this in the errataWith STM32CubeIDE v1.5.0, the option “Disable thread-safe initialization of local static objects (-fnothreadsafe-statics)” has changed the default value from “true” to “false”. This means that both flashmemory and RAM usages are slightly increased with respect to previous versions of STM32CubeIDE, with thebenefit of removing a potential race condition in the embedded code. To preserve the old behavior, make surethat the checkbox for the option is checked under [Project properties]>[C/C++ Build]>>>[MCU G++ Compiler]>.I did a google but can't find an explanation I can understand.Obviously statics arenotthread safe. Only variables on the stack are, plus variables stored in some structure which has the RTOS task number as the 2nd dimension (a popular method with old multiuser OSs).

author: ejeffrey
author_rank: Super Contributor
timestamp: August 04, 2023, 02:38:11 pm
content: Static variables are neither thread safe nor not thread safe, it depends on how you use them i.e., protected by a mutex. Technically that's true of automatic variables but *most* automatic variables are not shared across threads.Thread safe initialization means that the variables are protected from being initialized twice in two different threads. Since initialization happens automatically you don't have the option to make them thread safe yourself.

author: ejeffrey
author_rank: Super Contributor
timestamp: August 04, 2023, 02:58:23 pm
content: Note that this probably only affects static variables with non trivial initialization. If the initialized is a constant it will just be initialized at program load time with the rest of the data section. However if your initialized requires code execution I believe that happens the first time the function is called. There is a flag indicating that the variable is initialized and the first caller will do the initialization and set the flag. This is the part that the "thread safe initialization" affects, presumably creating a mutex to guard the initialization flag.

author: peter-h
author_rank: Super Contributor
timestamp: August 04, 2023, 08:49:09 pm
content: QuoteSurely a function likeint fred (int x){int y=3;int dd=0;int z=(x*y)+dd;return (z);}is re-entrant and thus thread-safe.The y=3 cannot be done at program init (i.e. it is not done with DATA, and DD=0 is also not done by placing dd into BSS; the compiler creates some code which lives at the start of that function, and it gets run every time that function is called, IME.How would you create non thread safe initialisation?

author: SiliconWizard
author_rank: Super Contributor
timestamp: August 04, 2023, 08:59:03 pm
content: It's a GCC option and "documented" (if we can call it that) here:https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Dialect-Options.html-fno-threadsafe-staticsNote that it's purely a C++ option, so if you don't use C++, it should have no effect.My understanding is that, contrary to C, for which initializers of static variables must be constants, in C++, local static variables can be initialized at run-time and thus there may be problems of thread-safety *for the initialization*. It's only about initialization. Obviously as you mentioned, using static variables may not be thread-safe per se otherwise. It all depends on how they are used.As a simple illustration, the following piece of code is not correct C and won't compile:Code:#include <stdlib.h>int Foo(int m){static int n = rand();return n + m;}But in C++, it is correct code and will compile.Example of GCC (C++ mode) output for ARM:with '-Os':Code:Foo(int):push{r3, r4, r5, lr}movr5, r0ldrr4, [pc, #36]; (2c <Foo(int)+0x2c>)ldrr3, [r4, #0]dmbishlslsr3, r3, #31bmi.n24 <Foo(int)+0x24>movr0, r4bl0 <__cxa_guard_acquire>R_ARM_THM_CALL __cxa_guard_acquirecbzr0, 24 <Foo(int)+0x24>bl0 <rand>R_ARM_THM_CALL randstrr0, [r4, #4]movr0, r4bl0 <__cxa_guard_release>R_ARM_THM_CALL __cxa_guard_releaseldrr0, [r4, #4]addr0, r5pop{r3, r4, r5, pc}nop.word0x00000000R_ARM_ABS32 .bssIt guards the initialization with calls to __cxa_guard_acquire and __cxa_guard_release.With '-Os -fno-threadsafe-statics':Code:Foo(int):push{r3, r4, r5, lr}movr5, r0ldrr4, [pc, #20]; (1c <Foo(int)+0x1c>)ldrr3, [r4, #0]lslsr3, r3, #31bmi.n16 <Foo(int)+0x16>bl0 <rand>R_ARM_THM_CALL randmovsr3, #1strr0, [r4, #4]strr3, [r4, #0]ldrr0, [r4, #4]addr0, r5pop{r3, r4, r5, pc}.word0x00000000R_ARM_ABS32 .bssIt doesn't guard initialization.So this may look a bit pointless in practice. But it's actually mandated by C++11 as far as I've seen.GCC just gives an option to disable it.If you don't use C++ or use C++ but don't use local static objects, there is absolutely nothing to be concerned about.

author: ejeffrey
author_rank: Super Contributor
timestamp: August 04, 2023, 10:34:20 pm
content: Quote from: peter-h on August 04, 2023, 08:49:09 pmThat option is only about static variables not automatic/stack allocated variables. Stack allocated variables indeed always have thread-safe initialization, although you can then do non-thread-safe activities with them, for instance if you store a pointer to a local variable somewhere another thread can access it.

author: peter-h
author_rank: Super Contributor
timestamp: August 05, 2023, 06:23:22 am
content: QuoteThe "static int n" is itselfnonthread safe; no need for the non-constant initialisation. The variable n will be placed in main RAM, not on the stack, and the only "benefit" of n being definedinsidea function is that its name is private.I have lots of functions like that, sometimes manipulating explicitly global variables, but such functions can be called from onlyoneRTOS task.Also calling rand() is probably not re-entrant, especially if the randomisation comes from a bit of hardware (usually the best way to do "truly random" numbers).So, I must be stupid (I certainly am compared to all you guys) because I don't get the deal here. The examples given are obviously not going to be re-entrant.Does this option mean thatanystatement which isinitiallyloading a local variable gets mutexed?

author: gf
author_rank: Super Contributor
timestamp: August 05, 2023, 12:22:15 pm
content: Quote from: peter-h on August 05, 2023, 06:23:22 amIt isdouble-checked locking, i.e. the test of the "is initialized" flag is primarily lock-free. The lock is only acquired if the flag indicates that the variable is not yet initialized. Once the variable has been initialized, the lock is not acquired any more.

author: peter-h
author_rank: Super Contributor
timestamp: August 05, 2023, 01:12:42 pm
content: This is well above my pay grade, so I am happy to see it is for C++ only which I have studiously avoidedI pay people on freelancer.com to knock up little win32 utilities, in MS VC++...I use mutexes extensively and had a nasty experience with the Newlib printf / heap family where they a) don't supply sources b) implement them as stubs c) fail to compile them as "weak" (I posted all the details) so I won't ask how GCC implements themMaybe it puts them on the heap? I am not sure one can make totally target-independent mutexes, which can be hidden wholly inside the language. You have to either use TEST/SET or call a stub which the dev then implements. In my target I use locks provided by FreeRTOS.

author: ejeffrey
author_rank: Super Contributor
timestamp: August 05, 2023, 05:19:15 pm
content: Quote from: peter-h on August 05, 2023, 06:23:22 amNo this is a fundamental misunderstanding of what it means to be thread safe.Thread safety can roughly be thought of as a property of code, not data. It is a promise that the code will behave correctly when called from multiple threads. It is 100% possible to do this with static variables, and the listed code is one way to do it. n is never modified after initialization (and could/should be declared const in C++). The compiler is required to ensure thread safe initialization so the whole construct is thread safe. Other ways to do this involve using mutexes or atomic operations to update the static variable.Generally you are responsible for making your code thread safe if needed, but when the compiler generates code for you, such as static initialization the standard specified if and how the generated must be thread safe.Thread local is mostly what you are talking about and is really a property of the data not the code. It means code than can or should be only accessed from a single thread. It's certainly one tool for creating thread safe code but not the only one.

author: ejeffrey
author_rank: Super Contributor
timestamp: August 05, 2023, 05:57:33 pm
content: Quote from: peter-h on August 05, 2023, 06:23:22 amThis is mostly irrelevent to the issue at hand, but on linux at least rand() is thread safe and rand_r is deprecated. I believe rand() is explicitly required to be psuedo-random, not a true random number generator. In any case, given only the code shown here, rand() is only called once, so it's thread safety is not important, the only point of that example was to initialize to a value that couldn't be a compile time constant.QuoteThe code siliconwizard posted is for sure silly, but is legal multi-thread safe C++ code with well defined behavior.QuoteSpecifically for static local variables that are initialized with run-time executed expressions, they will have a mutex and a flag guarding their initialization. It will generally use some form of double checked locking optimization to avoid acquiring the lock after initialization is complete and globally visible.static local variables that are initialized with constant values will simply be included in .bss or .data, no special synchronization is required. Automatic local variables are thread local and create a new copy for each invocation so they don't need any special handling.