2017-02-06 4 views
0

я реализовал спин-блокировку с C++ 11 атомной библиотекой:Почему спин-блокировка с std :: memory_order_relaxed выполняется правильно?

class SpinLock { 
    atomic_bool latch_; 

    public: 
    SpinLock() :latch_(false){ 
    } 
    void lock() { 
    while(tryLock() == false); 
    } 
    bool tryLock() { 
    bool b = false; 
    return latch_.compare_exchange_weak(b,true,std::memory_order_relaxed); 
    } 
    void unlock() { 
    latch_.store(false,std::memory_order_relaxed); 
    } 
}; 

Я проверил правильность порождая несколько потоков следующим образом:

static int z = 0; 
static SpinLock spinLock; 
static void safeIncrement(int run) { 
    while(--run >= 0) { 
    std::lock_guard<SpinLock> guard(spinLock); 
    ++z; 
    } 
} 

static void test(int nThreads =2) { 
    std::vector<std::thread*> workers(nThreads); 
    z = 0; 
    for(auto& ptr : workers) ptr = new std::thread(safeIncrement,1<<20); 
    for(auto ptr : workers) ptr->join(); 
    cout<<"after increment: " <<z << " out of " << (1<<20) * nThreads<<endl; 
    for(auto ptr : workers) delete ptr; 
} 
int main() { 

    test(4); 

    return 0; 

} 

Я удивлен, общее в конце добавляет как правильное значение с расслабленным порядком. В этой статье: http://en.cppreference.com/w/cpp/atomic/memory_order, расслабленный порядок означает «нет ограничений синхронизации или порядка», поэтому изменение от одного потока не должно быть видимым от других, не так ли? Почему все еще правильно?
(Тест выполняется на Intel (R) ядро ​​(TM) i5-3337U CPU @ 1.80GHz)

EDIT: (благодаря комментарий Максима) Обновлен код: инициализирует член данных в SpinLock, и обновление тестового кода.

+0

Обеспечить полный исходный код. –

+0

Нет инициализации 'SpinLock :: latch_', его начальное значение является неопределенным. –

ответ

1

Стандарт C++ 11 определяет самые слабые гарантии для атомных операций. Не все аппаратные средства могут точно соответствовать каждой самой слабой гарантии, поэтому компилятор и библиотекатор иногда должны «округлять» до более сильных операций. Например, все операции атомарного чтения-изменения-записи на x86 неявно имеют memory_order_acq_rel.

Кроме того, конкретные реализации аппаратной архитектуры могут иметь более сильные гарантии, чем говорят в руководствах по оборудованию. Например, в ранних версиях Itaniums реализована семантика памяти_order_acq_rel даже для некоторых аппаратных инструкций, которые обещали только memory_order_release.

Теоретически, ваш код может быть сбой на x86, так как для упорядочения памяти атомных операций требуется как аппаратное, так и компиляторное оборудование. Агрессивный компилятор мог законно перемещать нагрузку «z» (и, возможно, магазин тоже!) Вверх по операции tryLock, которая использует только memory_order_release.

+0

Точка зрения, которую я оставил, заключается в том, что «релиз» выполняет синхронизацию только тогда, когда (динамически) сопряжен с «приобретать» в * том же * местоположении. Поскольку вы объявили 'spinLock' статичным, и все операции над ним видны компилятору, компилятор мог заметить, что никогда не выполняются операции« приобретать »для' spinLock' и юридически меняют все ваши операции на переменную 'spinLock' на' memory_order_relaxed'. –

1

Я вижу, что по крайней мере GCC 6.3 на x86-64 генерирует такой же код для расслабления и для выпуска/приобретения. Поэтому неудивительно, что результаты одинаковы. Итак, чтобы увидеть разницу, вам может понадобиться более расслабленная архитектура памяти, чем TSO, которую предоставляет x86-64. Возможно, это может быть ARM.

+0

Возможно, что 'z' ожидает значение, потому что оно объявлено как' std :: atomic ', или' run' не является положительным, или потому что существует только один поток. –

0

Использование смягченного ограничения порядка на реализацию мьютекса - это рецепт катастрофы.
Мьютексы по определению предназначены для синхронизации данных между потоками. Термины приобретают и выпуск сильно связаны с мьютексом; вы приобретите мьютекс, измените данные, защищенные им, и релиз мьютекса, чтобы данные стали видимыми для другого потока, когда он приобретает один и тот же мьютекс.

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

В более сильной упорядоченной архитектуре, такой как X86, которая имеет неявную семантику получения/выпуска, вы избавитесь от этой реализации (отсюда ваши тесты будут успешными). Однако запустите его на архитектуре, которая использует более слабый порядок памяти, такой как Power или ARMv7, и у вас проблемы.

Заказы, которые вы предлагаете в комментариях, верны.

+0

Тем не менее, «операции с памятью, связанные с мьютексом», могут включать операции над самим мьютексом, правильно? –

+0

@LeoLai Операции над самим мьютексом являются атомарными, поскольку они только изменяют внутренний 'atomic_bool' - это то, что вы получаете даже с расслабленным упорядочением. Однако «операции с памятью, связанные с мьютексом», - это операции, выполняемые до или после мьютекса (в порядке выполнения программы). Поскольку мьютекс используется для синхронизации между потоками, он должен обеспечивать упорядочение между операциями памяти до «разблокировки/освобождения» и после «блокировки/получения» ..... – LWimsey

+0

..... «Разблокировка/выпуск 'на мьютексе _must_ запрещает операции памяти, предшествующие ему (в порядке выполнения), перемещаться вниз по мьютексу, , в то время как операция «блокировать/приобретать» _must_ предотвращает операции памяти, которые следуют за ним, перемещаясь по мьютексу. Эти заказы гарантированы установкой барьеров получения/выпуска на атомарных операциях в реализации мьютекса. – LWimsey

Смежные вопросы