Seems like this has come up for me enough in discussing C++ with others, that it might help to put together the information in this gist.
Standard C++ has since C++11 required that block-scope variables with static or thread storage duration get initialized in a thread safe way. Besides there being a StackOverflow answer - to a related question - showing some example assembly for C++11 that includes thread safe guards, there's also the C++ standard documents themselves.
What follows is the specific requirement by versions of the standard.
Per N1905 (the C++03 standard) 6.7 Declaration statement [stmt.dcl], subsection 4:
The zero-initialization (8.5) of all local objects with static storage duration (3.7.1) is performed before any other initial- ization takes place. A local object of POD type (3.9) with static storage duration initialized with constant-expressions is initialized before its block is first entered. An implementation is permitted to perform early initialization of other local objects with static storage duration under the same conditions that an implementation is permitted to statically initialize an object with static storage duration in namespace scope (3.6.2). Otherwise such an object is initialized the first time control passes through its declaration; such an object is considered initialized upon the completion of its initialization. If the initialization exits by throwing an exception, the initialization is not complete, so it will be tried again the next time control enters the declaration. If control re-enters the declaration (recursively) while the object is being initialized, the behavior is undefined.
Notice that it, makes no requirement on concurrent execution.
Meanwhile, C++11 onward does...
Per N3690 (the C++11 standard), 6.7 Declaration statement [stmt.dcl], subsection 4:
The zero-initialization (8.5) of all block-scope variables with static storage duration (3.7.1) or thread storage duration (3.7.2) is performed before any other initialization takes place. Constant initialization (3.6.2) of a block-scope entity with static storage duration, if applicable, is performed before its block is first entered. An implementation is permitted to perform early initialization of other block-scope variables with static or thread storage duration under the same conditions that an implementation is permitted to statically initialize a variable with static or thread storage duration in namespace scope (3.6.2). Otherwise such a variable is initialized the first time control passes through its declaration; such a variable is considered initialized upon the completion of its initialization. If the initialization exits by throwing an exception, the initialization is not complete, so it will be tried again the next time control enters the declaration. If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization. If control re-enters the declaration recursively while the variable is being initialized, the behavior is undefined.
The key sentence in the above text of the standard, is emboldened, and quoted here again for clarity:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
See N4296 (the C++14 standard), also 6.7 Declaration statement [stmt.dcl], subsection 4.
Per N4687 (the C++17 standard), the key requirement is present but moves to 9.7 Declaration statement [stmt.dcl], subsection 4.
Per N4861 (the C++20 standard), the key requirement is present but again moves, this time to 8.8 Declaration statement [stmt.dcl], subsection 4.
See N4950 (the C++23 standard), 8.8 Declaration statement [stmt.dcl], but now subsection 3 (instead of 4).