2016-02-08 1 views
4

В приведенном ниже коде показаны два способа получения общего состояния через атомный флаг. Поток читателя вызывает poll1() или poll2(), чтобы проверить, сигнализировал ли он флаг.Преимущества и недостатки как-то условного std :: atomic_thread_fence приобретают?

Опрос Вариант № 1:

bool poll1() { 
    return (flag.load(std::memory_order_acquire) == 1); 
} 

Опрос Вариант № 2:

bool poll2() { 
    int snapshot = flag.load(std::memory_order_relaxed); 
    if (snapshot == 1) { 
     std::atomic_thread_fence(std::memory_order_acquire); 
     return true; 
    } 
    return false; 
} 

Обратите внимание, что вариант # 1 был presented in an earlier question, и вариант # 2 похож на example code at cppreference.com.

Предполагая, что читатель согласится только исследовать общее состояние, если функция возвращает polltrue, является две poll функции как правильно и эквивалент?

Есть ли у варианта № 2 стандартное название?

Каковы преимущества и недостатки каждого варианта?

Возможно ли, что вариант № 2 будет более эффективным на практике? Возможно ли, чтобы он был менее эффективным?

Вот полный рабочий пример:

#include <atomic> 
#include <chrono> 
#include <iostream> 
#include <thread> 

int x; // regular variable, could be a complex data structure 

std::atomic<int> flag { 0 }; 

void writer_thread() { 
    x = 42; 
    // release value x to reader thread 
    flag.store(1, std::memory_order_release); 
} 

bool poll1() { 
    return (flag.load(std::memory_order_acquire) == 1); 
} 

bool poll2() { 
    int snapshot = flag.load(std::memory_order_relaxed); 
    if (snapshot == 1) { 
     std::atomic_thread_fence(std::memory_order_acquire); 
     return true; 
    } 
    return false; 
} 

int main() { 
    x = 0; 

    std::thread t(writer_thread); 

    // "reader thread" ... 
    // sleep-wait is just for the test. 
    // production code calls poll() at specific points 

    while (!poll2()) // poll1() or poll2() here 
     std::this_thread::sleep_for(std::chrono::milliseconds(50)); 

    std::cout << x << std::endl; 

    t.join(); 
} 
+0

Вариант 2 выглядит ужасно, как «двойная проверка блокировки антишаблона», и если вы используете его, я хотел бы видеть много комментариев объясняя, почему это не так (вероятно, со ссылкой здесь). Какая разница в производительности делает poll2? ('cos если ответ «не так много», я бы придерживался poll1 по принципу KISS). –

+0

Они выглядят более или менее одинаково для меня. Я повторю чувства Мартина Боннера, а также добавлю, что если вы заинтересованы в эффективности, то проще всего сделать это, чтобы профиль! – AndyG

+2

@MartinBonner ваши проблемы более или менее соответствуют мотивации для вопроса. Боковое примечание: дважды проверенная блокировка была только анти-шаблоном до C++ 11: http://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/ –

ответ

2

Я думаю, что могу ответить на большинство ваших вопросов.

Оба варианта, безусловно, правильны, но они не совсем эквивалентны из-за немного более широкой применимости автономных заборов (они эквивалентны с точки зрения того, что вы хотите выполнить, но автономный забор может применяться технически к другим вещам - представьте, включен ли этот код). Пример того, как отдельный забор отличается от забора магазина/выборки, объясняется в this post by Jeff Preshing.

Шаблон проверки-забора в варианте №2 не имеет имени, насколько я знаю. Однако это не редкость.

С точки зрения производительности, с моим g ++ 4.8.1 на x64 (Linux) сборка, сгенерированная , как вариантов сводится к одной инструкции нагрузки. Это неудивительно, учитывая, что x86 (-64) загружает и сохраняет все, все равно приобретает и освобождает семантику на уровне аппаратного обеспечения (x86 известен своей довольно сильной моделью памяти).

для ARM, хотя, в котором барьеры памяти компилировать до фактических отдельных инструкций, следующий вывод производится (с использованием gcc.godbolt.com с -O3 -DNDEBUG):

Для while (!poll1());:

.L25: 
    ldr  r0, [r2] 
    movw r3, #:lower16:.LANCHOR0 
    dmb  sy 
    movt r3, #:upper16:.LANCHOR0 
    cmp  r0, #1 
    bne  .L25 

Для while (!poll2());:

.L29: 
    ldr  r0, [r2] 
    movw r3, #:lower16:.LANCHOR0 
    movt r3, #:upper16:.LANCHOR0 
    cmp  r0, #1 
    bne  .L29 
    dmb  sy 

Вы можете видеть, что единственное отличие заключается в том, что синхронизация инструкция по ронизации (dmb) размещена - внутри петли для poll1, а после нее - для poll2.Таким образом, poll2 действительно более эффективен в этом реальном случае :-) (Но далее читайте, почему это не имеет значения, если они вызываются в цикле для блокировки до тех пор, пока не изменится флаг.)

Для ARM64 выход отличается, потому что существуют специальные инструкции по загрузке/хранению, которые имеют встроенные барьеры (ldar -> load-приобретать).

Для while (!poll1());:

.L16: 
    ldar w0, [x1] 
    cmp  w0, 1 
    bne  .L16 

Для while (!poll2());:

.L24: 
    ldr  w0, [x1] 
    cmp  w0, 1 
    bne  .L24 
    dmb  ishld 

Опять же, poll2 приводит к петле без каких-либо препятствий внутри него, и один снаружи, в то время как poll1 делает барьер каждый раз до конца.

Теперь, для которого на самом деле более требователен, требуется запуск теста, и, к сожалению, у меня нет настройки для этого. poll1 и poll2 могут быть в равной степени эффективными в этом случае, так как дополнительное время ожидания эффектов памяти для распространения внутри цикла может фактически не тратить время, если переменная флага является одним из тех эффектов, которые необходимо распространять в любом случае (т. е. общее время, затраченное до выхода петли, может быть одинаковым, даже если индивидуальные (встроенные) вызовы на poll1 занимают больше времени, чем poll2). Конечно, это предполагает цикл, ожидающий изменения флага - индивидуальные вызовы poll1do требуют больше работы, чем индивидуальные вызовы poll2.

Так что, я думаю, что в целом это довольно смело сказать, что poll2 никогда не должна быть значительно менее эффективной, чем poll1 и часто может быть быстрее, если компилятор может устранить ветвь, когда это встраиваемый (который, как представляется, в случае по крайней мере, эти три популярные архитектуры).

My (немного отличается) тестовый код для справки:

#include <atomic> 
#include <thread> 
#include <cstdio> 

int sharedState; 
std::atomic<int> flag(0); 

bool poll1() { 
    return (flag.load(std::memory_order_acquire) == 1); 
} 

bool poll2() { 
    int snapshot = flag.load(std::memory_order_relaxed); 
    if (snapshot == 1) { 
     std::atomic_thread_fence(std::memory_order_acquire); 
     return true; 
    } 
    return false; 
} 

void __attribute__((noinline)) threadFunc() 
{ 
    while (!poll2()); 
    std::printf("%d\n", sharedState); 
} 

int main(int argc, char** argv) 
{ 
    std::thread t(threadFunc); 
    sharedState = argc; 
    flag.store(1, std::memory_order_release); 
    t.join(); 
    return 0; 
} 
+0

Фантастический ответ. Это подтвердило все, что я думал, но не хотел тратить время на тщательный ответ. : -] – ildjarn

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