Подтвердить что ты не робот

Являются ли функционально-локальные статические мьютексы потокобезопасными?

В следующей программе я попытаюсь сделать функцию print поточной безопасностью с помощью объекта-объекта-объекта mutex:

#include <iostream>
#include <chrono>
#include <mutex>
#include <string>
#include <thread>


void print(const std::string & s)
{    
    // Thread safe?
    static std::mutex mtx;
    std::unique_lock<std::mutex> lock(mtx);
    std::cout <<s << std::endl;
}


int main()
{
    std::thread([&](){ for (int i = 0; i < 10; ++i) print("a" + std::to_string(i)); }).detach();
    std::thread([&](){ for (int i = 0; i < 10; ++i) print("b" + std::to_string(i)); }).detach();
    std::thread([&](){ for (int i = 0; i < 10; ++i) print("c" + std::to_string(i)); }).detach();
    std::thread([&](){ for (int i = 0; i < 10; ++i) print("d" + std::to_string(i)); }).detach();
    std::thread([&](){ for (int i = 0; i < 10; ++i) print("e" + std::to_string(i)); }).detach();
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

Это безопасно?

Мои сомнения возникают из этого вопроса, в котором представлен аналогичный случай.

4b9b3361

Ответ 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.

Ответ 2

Это не то же самое, что связанный вопрос по нескольким причинам.

Связанный вопрос - это не С++ 11, а ваш. В С++ 11 инициализация функционально-локальных статических переменных всегда безопасна. До С++ 11 это было безопасно только с некоторыми компиляторами, например. GCC и Clang по умолчанию для инициализации с потоком.

Связанный вопрос инициализирует ссылку, вызывая функцию, которая является динамической инициализацией и происходит во время выполнения. Конструктор по умолчанию для std::mutex равен constexpr, поэтому ваша статическая переменная имеет постоянную инициализацию, т.е. Мьютекс может быть инициализирован во время компиляции (или время ссылки), так что динамически во время выполнения нечего делать. Даже если несколько потоков одновременно вызывают функцию, там ничего не нужно делать, прежде чем использовать мьютекс.

Ваш код безопасен (если ваш компилятор правильно реализует правила С++ 11.)

Ответ 3

Пока мьютекс статичен, да.

Локальная, нестатическая система не будет безопасна. Если все ваши потоки не используют один и тот же стек, это также означает, что вы теперь изобрели память, в которой одна ячейка может хранить много разных значений одновременно, и просто ждут, когда Нобелевский комитет уведомит вас о следующей Нобелевской премии.

У вас должно быть какое-то "глобальное" (разделяемое) пространство памяти для мьютексов.