C++线程安全整数

Pau*_*way 17 c++ multithreading integer thread-safety

我目前为线程安全整数创建了一个C++类,它只是私有地存储一个整数,并且公共获取一个使用boost :: mutex的set函数,以确保一次只能对整数应用一个变量.

这是最有效的方法,我被告知互斥量是非常耗费资源的吗?该课程使用频繁,非常迅速,因此很可能成为瓶颈......

谷歌C++线程安全整数返回不清楚的视图和oppinions在不同架构上的整数操作的线程安全性.

有人说,在32位拱32位int是安全的,但64对32是不是因为"对齐"还有人说,这是编译器/操作系统特定的(我不怀疑).

我使用Ubuntu 9.10在32台机器上,一些具有双核等线程可以同时在不同核心上在一些情况下执行,并且我使用GCC 4.4的克++编译器.

提前致谢...

请注意: 我标记为"正确"的答案最适合我的问题 - 但是在其他答案中有一些优点,它们都值得一读!

Vic*_*iba 7

有C++ 0x原子库,还有一个正在开发的Boost.Atomic库,它使用无锁技术.


Mik*_* D. 6

它不是编译器和操作系统特定的,它是特定于体系结构的.编译器和操作系统进入它,因为它们是你工作的工具,但它们不是设置真实规则的工具.这就是为什么C++标准不会触及这个问题的原因.

我一生中从未听说过64位整数写入,可以将其分成两个32位写入,中途被中断.(是的,这是邀请其他人发布反例的.)具体来说,我从来没有听说过CPU的加载/存储单元允许错位的写入被中断; 中断源必须等待整个未对齐的访问完成.

要拥有可中断的加载/存储单元,必须将其状态保存到堆栈中......并且加载/存储单元将CPU的其余状态保存到堆栈中.如果加载/存储单元是可中断的,那么这将非常复杂,并且容易出错......并且您将获得的所有响应中断的延迟减少一个周期,最多只能在数十个周期内测量.完全不值得.

早在1997年,一位同事和我编写了一个用于多处理系统的C++队列模板.(每个处理器都有自己的OS运行,以及它自己的本地内存,所以这些队列只需要处理器之间共享的内存.)我们找到了一种方法,通过一次整数写入来改变队列状态,并将此写入视为原子操作.此外,我们要求队列的每一端(即读或写索引)由一个且仅一个处理器拥有.十三年后,代码仍然运行良好,我们甚至有一个处理多个读者的版本.

但是,如果要将64位整数写为原子,请将该字段与64位边界对齐.为什么要担心?

编辑:对于你在评论中提到的情况,我需要更多的信息来确定,所以让我举一个可以在没有专门的同步代码的情况下实现的东西的例子.

假设你有N个作家和一个读者.您希望编写者能够向读者发送事件信号.事件本身没有数据; 你只是想要一个事件计数,真的.

声明共享内存的结构,在所有作者和读者之间共享:

#include <stdint.h>
struct FlagTable
{   uint32_t flag[NWriters];
};
Run Code Online (Sandbox Code Playgroud)

(将其设为类或模板或您认为合适的任何内容.)

需要告诉每个编写器它的索引并给出一个指向该表的指针:

class Writer
{public:
    Writer(FlagTable* flags_, size_t index_): flags(flags_), index(index_) {}
    void SignalEvent(uint32_t eventCount = 1);
private:
    FlagTable* flags;
    size_t index;
}
Run Code Online (Sandbox Code Playgroud)

当作者想要发出一个或多个事件的信号时,它会更新其标志:

void Writer::SignalEvent(uint32_t eventCount)
{   // Effectively atomic: only one writer modifies this value, and
    // the state changes when the incremented value is written out.
    flags->flag[index] += eventCount;
}
Run Code Online (Sandbox Code Playgroud)

读者保留它看到的所有标志值的本地副本:

class Reader
{public:
    Reader(FlagTable* flags_): flags(flags_)
    {   for(size_t i = 0; i < NWriters; ++i)
            seenFlags[i] = flags->flag[i];
    }
    bool AnyEvents(void);
    uint32_t CountEvents(int writerIndex);
private:
    FlagTable* flags;
    uint32_t seenFlags[NWriters];
}
Run Code Online (Sandbox Code Playgroud)

要查明是否发生了任何事件,它只会查找更改的值:

bool Reader::AnyEvents(void)
{   for(size_t i = 0; i < NWriters; ++i)
        if(seenFlags[i] != flags->flag[i])
            return true;
    return false;
}
Run Code Online (Sandbox Code Playgroud)

如果发生了什么事,我们可以检查每个来源并获得事件计数:

uint32_t Reader::CountEvents(int writerIndex)
{   // Only read a flag once per function call.  If you read it twice,
    // it may change between reads and then funny stuff happens.
    uint32_t newFlag = flags->flag[i];
    // Our local copy, though, we can mess with all we want since there
    // is only one reader.
    uint32_t oldFlag = seenFlags[i];
    // Next line atomically changes Reader state, marking the events as counted.
    seenFlags[i] = newFlag;
    return newFlag - oldFlag;
}
Run Code Online (Sandbox Code Playgroud)

现在这一切都很重要吗?它是非阻塞的,也就是说,在Writer写东西之前你不能让读者睡觉.读者必须选择坐在等待AnyEvents()返回的自旋循环中true,这可以最大限度地减少延迟,或者每次都可以稍微睡一会儿,从而节省CPU但可以让很多事件积累起来.所以它总比没有好,但它并不是解决所有问题的方法.

使用实际的同步原语,只需要使用互斥锁和条件变量来包装此代码以使其正确阻塞:读取器将睡眠直到有事情要做.由于您使用带有标志的原子操作,实际上可以将互斥锁锁定的时间保持在最小值:Writer只需要锁定互斥锁足够长的时间来发送条件,而不是设置标志和读取器只需要在调用之前等待条件AnyEvents()(基本上,它就像上面的睡眠循环情况,但是有一个等待条件而不是睡眠调用).

  • 我知道这个答案很老,但这是错误的(并且在搜索结果中显得很高).如果有多个CPU,则不需要中断该指令以解决问题.如果两个独立的CPU启动SignalEvent,它们可能都加载,两者都计算,然后都存储,导致一个标志被覆盖.即使在单个现代CPU上,指令也可以重新排序,即使算法编码正确,也可以防止算法正常工作.Memory Fences或Atomic Compare and Swaps是制作这类简单算法的现代工具. (2认同)