Ответ 1
С++ 11
В С++ 11 и более поздних версиях инициализация функций-локальных статических переменных является потокобезопасной, поэтому ваш код выше гарантированно безопасен.
Таким образом, это практически на практике заключается в том, что компилятор вставляет любой необходимый шаблон в самой функции, чтобы проверить, инициализирована ли переменная до доступа. Однако в случае std::mutex
, реализованного в gcc
, clang
и icc
, инициализированное состояние является all-zeros, поэтому не требуется явная инициализация (переменная будет жить в all-zeros .bss
так что инициализация "свободна" ), как видно из сборка 1:
inc(int& i):
mov eax, OFFSET FLAT:_ZL28__gthrw___pthread_key_createPjPFvPvE
test rax, rax
je .L2
push rbx
mov rbx, rdi
mov edi, OFFSET FLAT:_ZZ3incRiE3mtx
call _ZL26__gthrw_pthread_mutex_lockP15pthread_mutex_t
test eax, eax
jne .L10
add DWORD PTR [rbx], 1
mov edi, OFFSET FLAT:_ZZ3incRiE3mtx
pop rbx
jmp _ZL28__gthrw_pthread_mutex_unlockP15pthread_mutex_t
.L2:
add DWORD PTR [rdi], 1
ret
.L10:
mov edi, eax
call _ZSt20__throw_system_errori
Обратите внимание, что начиная с строки mov edi, OFFSET FLAT:_ZZ3incRiE3mtx
он просто загружает адрес функции inc::mtx
local-local и вызывает на нем pthread_mutex_lock
без какой-либо инициализации. Код до этого, относящийся к pthread_key_create
, по-видимому, просто проверяет, присутствует ли вообще библиотека pthreads .
Однако не гарантируется, что все реализации будут реализовывать std::mutex
как all-zeros, поэтому вы можете в некоторых случаях нести накладные расходы на каждый вызов, чтобы проверить, был ли инициализирован mutex
. Объявление мьютекса вне функции могло бы избежать этого.
Здесь пример, контрастирующий два подхода с классом stand-in mutex2
с не-встроенным конструктором (поэтому компилятор может 't определить, что начальное состояние является all-zeros):
#include <mutex>
class mutex2 {
public:
mutex2();
void lock();
void unlock();
};
void inc_local(int &i)
{
// Thread safe?
static mutex2 mtx;
std::unique_lock<mutex2> lock(mtx);
i++;
}
mutex2 g_mtx;
void inc_global(int &i)
{
std::unique_lock<mutex2> lock(g_mtx);
i++;
}
Функция-локальная версия компилирует (на gcc
) в:
inc_local(int& i):
push rbx
movzx eax, BYTE PTR _ZGVZ9inc_localRiE3mtx[rip]
mov rbx, rdi
test al, al
jne .L3
mov edi, OFFSET FLAT:_ZGVZ9inc_localRiE3mtx
call __cxa_guard_acquire
test eax, eax
jne .L12
.L3:
mov edi, OFFSET FLAT:_ZZ9inc_localRiE3mtx
call _ZN6mutex24lockEv
add DWORD PTR [rbx], 1
mov edi, OFFSET FLAT:_ZZ9inc_localRiE3mtx
pop rbx
jmp _ZN6mutex26unlockEv
.L12:
mov edi, OFFSET FLAT:_ZZ9inc_localRiE3mtx
call _ZN6mutex2C1Ev
mov edi, OFFSET FLAT:_ZGVZ9inc_localRiE3mtx
call __cxa_guard_release
jmp .L3
mov rbx, rax
mov edi, OFFSET FLAT:_ZGVZ9inc_localRiE3mtx
call __cxa_guard_abort
mov rdi, rbx
call _Unwind_Resume
Обратите внимание на большое количество шаблонов, работающих с функциями __cxa_guard_*
. Во-первых, проверяется байт-флаг rip-relative, _ZGVZ9inc_localRiE3mtx
2 и если он не равен нулю, переменная уже инициализирована, и мы закончили и переходим в быстрый путь. Никаких атомных операций не требуется, потому что на x86 нагрузки уже имеют необходимую семантику получения.
Если эта проверка завершилась неудачно, мы перейдем к медленному пути, который по существу является формой двойной проверки блокировки: начальная проверка недостаточно для определения того, что переменная нуждается в инициализации, потому что здесь могут участвовать два или более потока. Вызов __cxa_guard_acquire
выполняет блокировку и вторую проверку и может либо перейти на быстрый путь (если другой поток одновременно инициализировал объект), либо может перейти к фактическому коду инициализации в .L12
.
Наконец, обратите внимание, что последние 5 инструкций в сборке не могут быть напрямую доступны из функции вообще, поскольку им предшествует безусловный jmp .L3
, и им ничего не прыгает. Они должны быть обработаны обработчиком исключений, если вызов конструктора mutex2()
выдает исключение в какой-то момент.
В целом, мы можем сказать, что при начальной загрузке инициализация с первым доступом является низкой и умеренной, потому что быстрый путь проверяет только один байтовый флаг без каких-либо дорогостоящих инструкций (а остальная часть самой функции обычно подразумевает как минимум два атомных операций для mutex.lock()
и mutex.unlock()
, но при этом происходит значительное увеличение размера кода.
Сравнение с глобальной версией, которая идентична, за исключением того, что инициализация происходит во время глобальной инициализации, а не перед первым доступом:
inc_global(int& i):
push rbx
mov rbx, rdi
mov edi, OFFSET FLAT:g_mtx
call _ZN6mutex24lockEv
add DWORD PTR [rbx], 1
mov edi, OFFSET FLAT:g_mtx
pop rbx
jmp _ZN6mutex26unlockEv
Функция меньше одной трети размера без каких-либо шаблонов инициализации.
До С++ 11
До С++ 11, однако, это, как правило, небезопасно, если только ваш компилятор не дает особых гарантий относительно того, как инициализируются статические локали.
Некоторое время назад, рассматривая аналогичную проблему, я рассмотрел сборку, сгенерированную Visual Studio для этого случая. Псевдокод для сгенерированного кода сборки для вашего метода print
выглядел примерно так:
void print(const std::string & s)
{
if (!init_check_print_mtx) {
init_check_print_mtx = true;
mtx.mutex(); // call mutex() ctor for mtx
}
// ... rest of method
}
init_check_print_mtx
- это генерируемая компилятором глобальная переменная, специфичная для этого метода, которая отслеживает, была ли инициализирована локальная статика. Обратите внимание, что внутри блока инициализации "один раз", защищенного этой переменной, переменная устанавливается в true до инициализации мьютекса.
Я, хотя это было глупо, поскольку это гарантирует, что другие потоки, участвующие в этом методе, пропустят инициализатор и используют неинициализированный mtx
- вместо альтернативы возможно инициализации mtx
более одного раза, но на самом деле делают это таким образом позволяет избежать проблемы с бесконечной рекурсией, которая возникает, если std::mutex()
должен был возвращаться в печать, и это поведение на самом деле обязано стандартом.
Nemo above упоминает, что это было исправлено (точнее, переопределено) в С++ 11, чтобы требовать ожидания всех гоночных потоков, что сделало бы это безопасным, но вам нужно будет проверить свой собственный компилятор для соблюдение. Я не проверял, действительно ли новая спецификация включает эту гарантию, но я бы не удивился, учитывая, что локальная статика была практически бесполезной в многопоточных средах без этого (за исключением, возможно, примитивных значений, которые не были любое поведение с проверкой и установкой, потому что они просто ссылаются непосредственно на уже инициализированное местоположение в сегменте .data).
1 Обратите внимание, что я изменил функцию print()
на несколько более простую функцию inc()
, которая просто увеличивает целое число в заблокированной области. Это имеет ту же структуру блокировки и последствия, что и оригинал, но позволяет избежать кучи кода, связанного с операторами <<
и std::cout
.
2 Используя c++filt
, это уменьшает до guard variable for inc_local(int&)::mtx
.