它不是特定于编译器和操作系统的,而是特定于体系结构的。编译器和操作系统加入其中是因为它们是您使用的工具,但它们不是设置真正规则的工具。这就是 C++ 标准不会触及这个问题的原因。
我一生中从未听说过 64 位整数写入,它可以分成两个 32 位写入,在中途被中断。 (是的,这是邀请其他人发布反例。)具体来说,我从未听说过 CPU 的加载/存储单元允许中断未对齐的写入。中断源必须等待整个未对齐的访问完成。
要拥有一个可中断的加载/存储单元,它的状态必须保存到堆栈中……而加载/存储单元将 CPU 的其余状态保存到堆栈中。如果加载/存储单元是可中断的,这将非常复杂,并且容易出错......您将获得的只是响应中断的延迟减少一个周期 ,充其量是在几十个周期内测量的。完全不值得。
早在 1997 年,我和一位同事编写了一个用于多处理系统的 C++ 队列模板。 (每个处理器都有自己的操作系统运行,以及自己的本地内存,因此这些队列仅用于处理器之间共享的内存。)我们想出了一种方法,通过单个整数写入使队列更改状态,并将此写入视为原子操作。此外,我们要求队列的每一端(即读取或写入索引)由一个且仅一个处理器拥有。十三年后,代码仍然运行良好,我们甚至有一个可以处理多个阅读器的版本。
不过,如果您想将 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;
}
现在这一切的大问题是什么?它是非阻塞的,也就是说你不能让 Reader 休眠,直到 Writer 写东西。 Reader 必须在等待AnyEvents() 返回true 的自旋循环中做出选择,这可以最大限度地减少延迟,或者它可以每次都休眠一点,这可以节省 CPU,但可能会堆积很多事件。所以有总比没有好,但不是万能的。
使用实际的同步原语,只需要用互斥锁和条件变量包装这段代码以使其正确阻塞:Reader 会休眠直到有事可做。由于您对标志使用了原子操作,因此您实际上可以将互斥锁锁定的时间保持在最低限度:编写器只需将互斥锁锁定足够长的时间即可发送条件,而无需设置标志,而读取器只需在调用AnyEvents() 之前等待条件(基本上,它就像上面的睡眠循环案例,但使用等待条件而不是睡眠调用)。