在没有互斥体的 C++11 中实现共享整数计数器的最简单方法:

Posted

技术标签:

【中文标题】在没有互斥体的 C++11 中实现共享整数计数器的最简单方法:【英文标题】:Easiest way to implement shared integer counter in C++11 without mutexes: 【发布时间】:2014-03-17 02:30:48 【问题描述】:

假设我们有以下代码来计算某事发生的次数:

int i=0;
void f() 
   // do stuff  . . .
   if(something_happens) ++i;


int main() 
    std::vector<std::thread> threads;
    for(int j = 0; j< std::thread::hardware_concurrency(); ++j) 
        threads.push_back(std::thread(f));
    

    std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread_join));
    std::cout << "i = " << i << '\n';

就目前而言,i 上有一个明确的竞争条件。使用 C++11,(1) 消除这种竞争条件的最简单方法是什么,以及 (2) 最快的方法是什么?最好不使用互斥锁。谢谢。

更新:使用注释来使用原子,我得到了一个在英特尔编译器版本 13 下编译的工作程序:

#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
#include <algorithm>

std::atomic<unsigned long long> i = 0;

void f(int j) 
    if(j%2==0) 
        ++i;
      


int main() 
    std::cout << "Atomic i = " << i << "\n";
    int numThreads = 8; //std::thread::hardware_concurrency() not yet implemented by Intel
    std::vector<std::thread> threads;
    for(int k=0; k< numThreads; ++k) 
        threads.push_back(std::thread(f, k));
    

    std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));
        std::cout << "Atomic i = " << i << "\n";
    

【问题讨论】:

原子。如果你使用的是GCC,搜索__sync builtins,否则我不知道。 您需要一个有意义的正在进行的计数,还是只需要所有线程结束后的最终计数? 【参考方案1】:

您可能想查看atomic types。您无需锁定/互斥即可访问它们。

【讨论】:

+1 用于提及 C++11 &lt;atomic&gt;,错过了问题标记为 C++11 :D【参考方案2】:

我们通过声明一个数组 [nThreads] 解决了一个类似的问题,然后我们给每个线程一个从 0 到 n 的 id,然后线程可以安全地写入数组中的位置。然后你可以对数组求和以获得总和。但是,这仅在您不需要在所有线程都死之前对数组求和时才有用。

为了更高效,我们在每个线程上都有一个本地计数器,然后在线程死亡之前将其附加到数组中。

示例(伪代码:)

counter[nThreads];

thread(int id)

    // Do stuff
    if(something happened)
       counter[id]++;   

counter[nThreads];

thread(int id)

    int localcounter = 0;
    //Do stuff
    if(something happened)
       localcounter++;   

    //Thread is about to die
    counter[id] = localcounter;

【讨论】:

+1,不错的概念:这对于更复杂的数据可能很方便。这就像一个简单的 map-and-reduce。 @DarkDust 打败我说这只是 map-reduce 你掉进了经典陷阱:你没有在每个变量之间填充数组,至少相当于缓存行(128 字节)。正因为如此,你没有注意到它,但硬件访问是由 MESI 协议互锁的。 @v.oddou 这就是我们采用第二种方法的原因。但这真是个好主意!【参考方案3】:

您可以使用InterlockedIncrement 函数。

MSDN 上以 Synchronization Functions 的名义记录了许多以原子方式改变变量的函数 - 它们可能对您有用。

【讨论】:

需要它同时在 Linux 和 Windows 上工作。但如果需要,我会使用该代码。 Boost:: atomic 是跨平台的!【参考方案4】:

这不仅仅是一个竞争条件,如果你的编译器决定这样做,你可能根本无法在线程之间传递实际的 i 值。

显然atomic 是个好方法。互斥锁也是一个好方法,当你没有碰撞时,它们和原子一样快。只有当他们真正需要让内核摆弄sleepingready 线程队列时,它们才会变慢。如果等待信号不使用condition variable,可能会变得很麻烦,在这种情况下,您可能必须等待调度内核滴答声才能让您的ready 线程成为running,这可能很长(30 毫秒) .

不过,原子可以让您达到最佳状态,甚至可能比 condition variables 更容易维护,因为不必关心 spurious 事件和 notify_onenotify_all 等。

如果您检查 C++11 制造的 STL 的 shared_ptr 基类,它们包含的 base_count 或(base_shared_count 或其他)完全符合您的需要。 如果愿意,您还可以检查新的 boost::shared_count 实现。

【讨论】:

以上是关于在没有互斥体的 C++11 中实现共享整数计数器的最简单方法:的主要内容,如果未能解决你的问题,请参考以下文章

WaitHandle学习笔记

如何在Clojure中实现整数的计数排序?

Go语言互斥锁

如何在 C 中实现无锁共享标志?

Boost中实现线程安全

锁定未锁定的互斥体的效率如何?互斥锁的成本是多少?