C ++线程安全整数

时间:2010-04-28 12:43:41

标签: c++ multithreading integer thread-safety

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

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

Googleing C ++线程安全整数会针对不同体系结构上的整数操作的线程安全性返回不清楚的视图和操作。

有人说32位拱门上的32位int是安全的,但是32位的64位不是由于“对齐”。其他人说它是编译器/操作系统特定的(我不怀疑)。

我在32位机器上使用Ubuntu 9.10,有些具有双核,因此在某些情况下可能会在不同的内核上同时执行线程,而我正在使用GCC 4.4的g ++编译器。

提前致谢...

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

6 个答案:

答案 0 :(得分:7)

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

答案 1 :(得分:6)

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

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

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

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

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

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

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

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

#include <stdint.h>
struct FlagTable
{   uint32_t flag[NWriters];
};

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

需要告诉每个作者其索引并给出指向该表的指针:

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;
}

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

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;
}

读者会保留所见过的所有旗帜值的本地副本:

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];
}

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

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

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

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;
}

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

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

答案 2 :(得分:4)

C ++没有真正的原子整数实现,大多数常见的库都没有。

考虑这样一个事实,即使所述实现存在,它也必须依赖某种互斥体 - 因为你无法保证所有体系结构的原子操作。

答案 3 :(得分:4)

当您使用GCC时,根据您要对整数执行的操作,您可能会使用GCC's atomic builtins

这些可能比互斥锁快一点,但在某些情况下仍然比“正常”操作慢很多。

答案 4 :(得分:2)

对于完整的通用同步,正如其他人已经提到的那样,传统的同步工具非常需要。但是,对于某些特殊情况,可以利用硬件优化。具体来说,大多数现代CPU支持原子增量和减少整数。 GLib库对此有非常好的跨平台支持。从本质上讲,该库包装了CPU&amp;这些操作的编译器特定汇编代码,默认为无法使用的互斥保护。它当然不是非常通用的,但如果你只想维持一个计数器,这可能就足够了。

答案 5 :(得分:2)