如何实现原子计数器

42
尝试创建一个独特的ID生成函数,得到了以下结果:
std::atomic<int> id{0};
int create_id() {
    id++;
    return id.load();
}

但是我假设这个函数有可能返回相同的值两次,对吗?例如,线程A调用该函数,增加了值,但是在线程B进来并增加了值之后,A停止,最后A和B都返回相同的值。
因此,使用互斥锁,该函数可能如下所示:
std::mutex mx;
int id = 0;
int create_id() {
    std::lock_guard<std::mutex> lock{mx};
    return id++;
}

我的问题是:是否可能仅使用原子操作来创建从计数器生成唯一整数值的行为?我之所以问这个问题,是因为我需要生成大量的ID,但听说互斥锁的速度很慢。
3个回答

69

28
默认情况下,std::atomic<int> 未初始化。由于此特定实例具有静态存储期,因此它会被初始化为0,但如果 id 是类中的一个字段,就需要在 std::atomic<int> id 后添加 {0} 来初始化。 - Alex Reinking
1
请注意,还有一个很好的 std::atomic_int 别名。 - juzzlin

10

你的两段代码实现了不同的功能。

id++;
return id.load();

该代码会增加id的值,然后返回增加后的值。

std::lock_guard<std::mutex> lock{mx};
return id++;

那段代码返回增加前的值。

要实现第一段代码试图做的事情,正确的代码是

return ++id;

实现第二个代码所做的正确代码是:

return id++;

9

互斥锁太过浪费资源。

没有预先增量原子操作(但你可以返回前一个值并将其加一)。

正如Pete所指出的那样,你的第一个代码块试图进行预增量(返回增量的结果)。

执行return ++id可以,但等同于fetch_add(1) + 1;使用缓慢的默认序列一致内存顺序。在这里没有必要,事实上你可以使用一个松散的内存顺序

如果你真的想要使用一个全局变量作为原子变量,正确(也是最快的)的代码是:

int create_id() {
    static std::atomic<int> id{0};
    return id.fetch_add(1, std::memory_order_relaxed) + 1;
}

注:

如果您只想使用后置递增,可以省略+ 1

在Intel CPU(x86)上使用std::memory_relaxed没有区别,因为fetch_add是一个读-修改-写操作,并且总线必须被锁定(使用lock汇编指令)。但是,在松散的体系结构上,它确实有所不同。

我不想用'id'来弄脏全局命名空间,所以将其作为函数中的静态变量。然而,在这种情况下,您必须确保在您的平台上,这不会导致实际的初始化代码。例如,如果需要调用非constexpr的构造函数,则需要测试静态变量是否已经初始化。幸运的是,整数原子的值初始化构造函数是constexpr的,因此以上代码导致常量初始化

否则,您应该将其设置为包装此功能的类的静态成员,并将初始化放在其他地方。


在C++中,不需要使用ATOMIC_VAR_INIT。根据你回答中的链接:“这个宏主要是为了与C兼容而提供的;它的行为与std::atomic的构造函数相同。” - bolov
啊,没错。原子类型的值初始化构造函数是constexpr的,因此也会导致常量初始化。这很有道理,因为编译器知道自己在做什么 ;)。我会稍微更新一下我的答案。 - Carlo Wood

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接