Writing a (spinning) thread barrier using c++11 atomics

Я пытаюсь ознакомиться с атомикой c++11, поэтому я попытался написать барьерный класс для потоков (прежде чем кто-то пожалуется на неиспользование существующих классов: это больше для обучения/самосовершенствования, чем из-за реальной необходимости). мой класс выглядит следующим образом:

class barrier
{
private:
    std::atomic<int> counter[2];
    std::atomic<int> lock[2];
    std::atomic<int> cur_idx;
    int thread_count;
public:
    //constructors...
    bool wait();
};

Все члены инициализированы нулем, кроме thread_count, который хранит соответствующий счетчик. Я реализовал функцию ожидания как

int idx  = cur_idx.load();
if(lock[idx].load() == 0)
{
    lock[idx].store(1);
}
int val = counter[idx].fetch_add(1);
if(val >= thread_count - 1)
{
    counter[idx].store(0);
    cur_idx.fetch_xor(1);
    lock[idx].store(0);
    return true;
}
while(lock[idx].load() == 1);
return false;

Однако при попытке использовать ее с двумя потоками (thread_count равно 2) первый поток попадает в цикл ожидания просто отлично, но второй поток не разблокирует барьер (кажется, он даже не доходит до int val = counter[idx].fetch_add(1);, но я не слишком уверен в этом. Однако когда я использую gcc atomic-intrinsics, используя volatile int вместо std::atomic и записывая wait в следующем виде:

int idx = cur_idx;
if(lock[idx] == 0)
{
    __sync_val_compare_and_swap(&lock[idx], 0, 1);
}
int val = __sync_fetch_and_add(&counter[idx], 1);
if(val >= thread_count - 1)
{
    __sync_synchronize();
    counter[idx] = 0;
    cur_idx ^= 1;
    __sync_synchronize();
    lock[idx] = 0;
    __sync_synchronize();
    return true;
}
while(lock[idx] == 1);
return false;

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

  1. Мне повезло со второй реализацией и мой алгоритм - дерьмо
  2. Я не до конца понял std::atomic и есть проблема с первым вариантом (но не со вторым)
  3. Это должно работать, но экспериментальная реализация для библиотек c++11 не настолько зрелая, как я надеялся

Для справки, я использую 32-битный mingw с gcc 4. 6.1

Код вызова выглядит так:

spin_barrier b(2);
std::thread t([&b]()->void
{
    std::this_thread::sleep_for(std::chrono::duration<double>(0.1));
    b.wait();
});
b.wait();
t.join();

Поскольку mingw не имеет заголовков jet, я использую для этого самописную версию, которая в основном обертывает соответствующие функции pthread (прежде чем кто-то спросит: да, это работает без барьера, так что с обертыванием проблем быть не должно). Любые соображения будут приняты с благодарностью.

edit: Пояснение к алгоритму, чтобы было понятнее:

  • thread_count - количество потоков, которые должны ждать барьера (так что если thread_count потоков находятся в барьере, все могут покинуть барьер).
  • lock устанавливается в единицу, когда первый (или любой) поток входит в барьер.
  • counter считает, сколько потоков находится внутри барьера, и атомарно увеличивается один раз для каждого потока
  • если counter>=thread_count, все потоки находятся внутри барьера, поэтому counter и lock обнуляются
  • иначе поток ждет, пока lock станет нулем
  • при следующем использовании барьера разные переменные (counter, lock) используются для того, чтобы не возникало проблем, если потоки все еще ожидают первого использования барьера (например. например, они были вытеснены при снятии барьера)

edit2: Теперь я протестировал это, используя gcc 4.5.1 под linux, где обе версии, похоже, работают просто отлично, что, похоже, указывает на проблему с std::atomic от mingw, но я все еще не до конца уверен, так как изучение заголовка подтвердило, что большинство функций просто вызывают соответствующий gcc-atomic, а значит, разницы между двумя версиями быть не должно

8
задан Grizzly 14 November 2011 в 13:09
поделиться