Multithreading 如何使用std::atomic实现可重用的线程屏障

Multithreading 如何使用std::atomic实现可重用的线程屏障,multithreading,c++11,Multithreading,C++11,我有N个线程执行各种任务,这些线程必须定期与线程屏障同步,如下面3个线程和8个任务所示。| |表示时间障碍,所有线程必须等到8个任务完成后才能重新开始 Thread#1 |----task1--|---task6---|---wait-----||-taskB--| ... Thread#2 |--task2--|---task5--|-------taskE---||----taskA--| ... Thread#3 |-task3-|---task4--|

我有N个线程执行各种任务,这些线程必须定期与线程屏障同步,如下面3个线程和8个任务所示。| |表示时间障碍,所有线程必须等到8个任务完成后才能重新开始

Thread#1  |----task1--|---task6---|---wait-----||-taskB--|          ...
Thread#2  |--task2--|---task5--|-------taskE---||----taskA--|       ...
Thread#3  |-task3-|---task4--|-taskG--|--wait--||-taskC-|---taskD   ...
我找不到一个可行的解决方案,认为这本关于信号灯的小书很鼓舞人心。我提出了一个使用std::atomic的解决方案,如下所示,它“似乎”使用三个std::atomic。 我担心我的代码在角落的情况下会崩溃,因此引用了动词。那么你能分享一下关于验证这些代码的建议吗?你有更简单的防愚代码吗

std::atomic<int> barrier1(0);
std::atomic<int> barrier2(0);
std::atomic<int> barrier3(0);

void my_thread()
{

  while(1) {
    // pop task from queue
    ...
    // and execute task 
    switch(task.id()) {
      case TaskID::Barrier:
        barrier2.store(0);
        barrier1++;
        while (barrier1.load() != NUM_THREAD) {
          std::this_thread::yield();
        }
        barrier3.store(0);
        barrier2++;
        while (barrier2.load() != NUM_THREAD) {
          std::this_thread::yield();
        }
        barrier1.store(0);
        barrier3++;
        while (barrier3.load() != NUM_THREAD) {
          std::this_thread::yield();
        }
       break;
     case TaskID::Task1:
       ...
     }
   }
}
std::原子屏障1(0);
标准:原子屏障2(0);
标准:原子屏障3(0);
使我的_线程无效()
{
而(1){
//从队列中弹出任务
...
//并执行任务
开关(task.id()){
案例TaskID::屏障:
路障2.商店(0);
barrier1++;
while(barrier1.load()!=NUM_线程){
std::this_thread::yield();
}
障碍物3.商店(0);
barrier2++;
while(barrier2.load()!=NUM_线程){
std::this_thread::yield();
}
障碍1.商店(0);
barrier3++;
while(barrier3.load()!=NUM_线程){
std::this_thread::yield();
}
打破
案例TaskID::Task1:
...
}
}
}
Boost提供了一个线程库,作为C++11标准线程库的扩展。如果使用Boost是一种选择,那么您不应该看得更远

如果您必须依赖标准库设施,那么您可以基于
std::mutex
std::condition\u variable
推出自己的实现,而不需要太多麻烦

class Barrier {
    int wait_count;
    int const target_wait_count;
    std::mutex mtx;
    std::condition_variable cond_var;

    Barrier(int threads_to_wait_for)
     : wait_count(0), target_wait_count(threads_to_wait_for) {}

    void wait() {
        std::unique_lock<std::mutex> lk(mtx);
        ++wait_count;
        if(wait_count != target_wait_count) {
            // not all threads have arrived yet; go to sleep until they do
            cond_var.wait(lk, 
                [this]() { return wait_count == target_wait_count; });
        } else {
            // we are the last thread to arrive; wake the others and go on
            cond_var.notify_all();
        }
        // note that if you want to reuse the barrier, you will have to
        // reset wait_count to 0 now before calling wait again
        // if you do this, be aware that the reset must be synchronized with
        // threads that are still stuck in the wait
    }
};

这个解决方案是正确的(非常合理的)假设是所有线程在
inter\u wait\u count
溢出之前离开等待。

对于原子变量,使用三个原子变量作为屏障只是过激,只会使问题复杂化。您知道线程的数量,因此每次线程进入屏障时,您可以简单地以原子方式递增一个计数器,然后旋转直到计数器变大或等于N。类似于这样:

void barrier(int N) {
    static std::atomic<unsigned int> gCounter = 0;
    gCounter++;
    while((int)(gCounter - N) < 0) std::this_thread::yield();
}

我想指出的是,在,
wait_count
应在执行
cond_var.notify_all()之前重置为0


这是因为当第二次调用屏障时,if条件将始终失败,if
wait\u count
不会重置为0。

既然您知道信号量,那么使用信号量有什么错?在C++11标准库中没有本机std::semaphore,所以您必须使用std::mutex和std::condition\u变量。这有帮助吗?我可以通过一次投票以一个重复的方式结束,但我确信如果是同一个问题。@R.MartinhoFernandes有趣的问题是我们是否想要一个旋转的解决方案。通常,屏障不会以这种方式实现,因为预期一些线程将不得不等待其他线程完成,我们不希望在这段时间内阻塞繁忙等待的内核。基于
condition\u variable
将等待线程发送到睡眠状态的解决方案在这里似乎更合适。你是对的,这是关键问题。我的建议是一个旋转的解决方案,因此可能没有那么有效。我将切换到找到的[condition_variable]。升压屏障不可重复使用,代码中作为注释包含的注释标记了将wait_计数重置为0的难度。我在编写代码时遇到了同样的问题,即只有2个原子变量,存在死锁问题。@user3636086 boost屏障是可重用的。当所有线程到达后,它会自动重置,您可以随时调用wait。@user3636086我添加了一些关于如何实现重置的内容。简单有效。我已经在我的代码中进行了尝试,工作正常,但是由于我的线程池的性质,我不得不在while(gCountergCounter
:据我所知,您的线程不能保证在屏障调用之间同步。因此,一个线程可能会向前运行,而另一个线程仍然在屏障内被抢占,并在延迟线程离开屏障之前调用下一个屏障。在这种情况下,计数器可能会跳过延迟线程正在等待的
N
,导致其死锁。我想,你对
gCounter
的问题是你想让计数器环绕?我已经编辑了我的答案,以便在这些情况下工作:当您期望溢出时,需要使用无符号整数类型。请注意,如果您的线程是Linux上的高优先级实时FIFO线程,则此代码可能a)占用cpu,b)导致优先级反转。不要使用此代码。只需使用
std::mutex
+
std::condition_variable
@MaximYegorushkin我不知道a)和b)指的是什么,但我的印象是,您还没有完全阅读我的答案:我特别指出,除非每个核心最多有一个线程,否则不应删除
yield()
调用。只要未达到屏障的线程继续运行,此代码就不会锁定。@cmaster它在任何情况下都不会死锁,但它可能会大大降低系统的速度,即使线程数等于或略小于物理内核数。忙碌等待的问题在于,你基本上是在欺骗系统,让它认为你有很多工作要做。因此,在调度程序分配的CPU时间和CPU消耗的实际电能方面,您将获得大量资源。如果您决定将这些资源用于在单个变量上快速循环,这就是您的决定。
void barrier(int N) {
    static std::atomic<unsigned int> gCounter = 0;
    gCounter++;
    while((int)(gCounter - N) < 0) std::this_thread::yield();
}
unsigned int lastBarrier = 0;
while(1) {
    switch(task.id()) {
        case TaskID::Barrier:
            barrier(lastBarrier += processCount);
            break;
    }
}