Проблема синхронизации потоков с переменными c ++ std :: atomic - PullRequest
4 голосов
/ 14 мая 2019

Следующая программа дает мне неожиданное поведение, когда время от времени выводит «плохой» вывод.Предполагается, что два потока синхронизируются с использованием двух переменных std :: atomic 's_lock1' и 's_lock2'.В func2, чтобы установить переменную 's_var' в 1, он должен атомарно хранить ненулевое значение в 's_lock2', а другой поток (func1) еще не должен обновлять переменную 's_lock1'.Однако как-то в func1 он печатает неожиданный «плохой» вывод.Инструкция s_lock2.load (), кажется, вместо этого возвращает false.Что-то не так с этим фрагментом кода?Это проблема, связанная с упорядочением памяти?

Я запускаю это на 8-ядерном сервере Linux с установленным Centos 7.Любая помощь с благодарностью.

#include <iostream>
#include <thread>
#include <atomic>
#include <unistd.h>

std::atomic_uint s_lock1 = 0;
std::atomic_uint s_lock2 = 0;
std::atomic_uint s_var = 0;

static void func1()
{
    while (true) {
        s_lock1.store(1, std::memory_order_release);
        if (s_lock2.load(std::memory_order_acquire) != 0) {
            s_lock1.store(0, std::memory_order_release);
            continue;
        }
        if (s_var.load(std::memory_order_acquire) > 0) {
            printf("bad\n");
        }
        usleep(1000);
        s_lock1.store(0, std::memory_order_release);
    }
}

static void func2()
{
    while (true) {
        s_lock2.store(1, std::memory_order_release);
        if (s_lock1.load(std::memory_order_acquire) != 0) {
            s_lock2.store(0, std::memory_order_release);
            continue;
        }
        s_var.store(1, std::memory_order_release);
        usleep(5000);
        s_var.store(0, std::memory_order_release);
        s_lock2.store(0, std::memory_order_release);
    }
}

int main()
{
    std::thread t1(func1);
    std::thread t2(func2);
    t1.join();
    t2.join();
}

1 Ответ

3 голосов
/ 14 мая 2019

Этот алгоритм блокировки может выйти из строя из-за буферов хранилища в процессорах Intel: хранилища не попадают непосредственно в кэш 1-го уровня, а на некоторое время ставятся в очередь в буфере хранилища и, следовательно, невидимы для другого процессора в течение этого времени:

Для оптимизации производительности выполнения команд архитектура IA-32 допускает отклонения от модели строгого упорядочения, называемой упорядочением процессоров в процессорах семейства Pentium 4, Intel Xeon и P6. Эти вариации упорядочения процессора (называемые здесь моделью упорядочения памяти) позволяют операциям повышения производительности, таким как чтение, опережать буферизованные записи. Целью любого из этих вариантов является увеличение скорости выполнения команд, в то время какподдержание согласованности памяти даже в многопроцессорных системах.

Буферы хранилища необходимо очистить, чтобы эта блокировка работала, используя std::memory_order_seq_cst для хранилищ для блокировок (по умолчаниюПорядок памяти для загрузок и хранилищ можно просто сделать s_lock1 = 1;, например).std::memory_order_seq_cst для хранилищ приводит к тому, что компилятор генерирует xchg инструкцию или вставку mfence инструкцию после хранилища, оба из которых делают эффект хранилища видимым для других процессоров:

Атомные операции помечены memory_order_seq_cst не только упорядочивает память так же, как упорядочение при отпускании / получении (все, что происходило до того, как хранилище в одном потоке становится видимым побочным эффектом в потоке, который выполнил загрузку), но также устанавливает единый общий порядок изменениявсех атомарных операций, помеченных .Последовательное упорядочение может быть необходимо для ситуаций с несколькими производителями и несколькими потребителями, когда все потребители должны наблюдать за действиями всех производителей, происходящими в одном и том же порядке.Полное последовательное упорядочение требует полной инструкции процессора CPU для всех многоядерных систем.Это может стать узким местом в производительности, поскольку оно заставляет затронутые обращения к памяти распространяться на каждое ядро.

Рабочий пример:

std::atomic<unsigned> s_lock1{0};
std::atomic<unsigned> s_lock2{0};
std::atomic<unsigned> s_var{0};

void func1() {
    while(true) {
        s_lock1.store(1, std::memory_order_seq_cst);
        if(s_lock2.load(std::memory_order_seq_cst) != 0) {
            s_lock1.store(0, std::memory_order_seq_cst);
            continue;
        }
        if(s_var.load(std::memory_order_relaxed) > 0) {
            printf("bad\n");
        }
        usleep(1000);
        s_lock1.store(0, std::memory_order_seq_cst);
    }
}

void func2() {
    while(true) {
        s_lock2.store(1, std::memory_order_seq_cst);
        if(s_lock1.load(std::memory_order_seq_cst) != 0) {
            s_lock2.store(0, std::memory_order_seq_cst);
            continue;
        }
        s_var.store(1, std::memory_order_relaxed);
        usleep(5000);
        s_var.store(0, std::memory_order_relaxed);
        s_lock2.store(0, std::memory_order_seq_cst);
    }
}

int main() {
    std::thread t1(func1);
    std::thread t2(func2);
    t1.join();
    t2.join();
}
...