使访问未签名字符线程安全(原子化)

6
我知道之前有类似的问题被提出过,也知道这个操作很可能根本不是原子操作,但我还是出于好奇心和希望找到使其成为原子操作的方法而提出了这个问题。
情况如下:在一个结构体中,有一个名为"Busy"的无符号字符变量(它可以被移到其他位置并独立存在)。
这个变量"Busy"由两个并发线程修改,一个在线程调度时设置位,另一个在调度操作完成后清除位。
目前,调度操作看起来像这样:
while(SEC.Busy&(1 << SEC.ReqID))
    if(++SEC.ReqID == 5) SEC.ReqID = 0;
sQuery.cData[2] = SEC.ReqID;

当清除位掩码时,情况看起来像这样:
SEC.Busy &= ~(1 << sQuery->cData[2]);

cData [2] 基本上携带有关于网络中使用哪个插槽的信息,并通过另一个线程的回调返回。

现在问题是:如果可能的话,如何确保SEC.Busy(这是唯一的变量)在两个尝试同时更改它的线程中不会被分开而不使用互斥,临界区或类似物品?

我还尝试将SEC.Busy的内容分配给一个本地变量,然后更改该变量,然后再写回变量,但不幸的是,此操作似乎也不是原子操作。

目前我正在使用Borland C ++ Builder 6,但GCC解决方案也可以。

非常感谢。


1
在单个字符上设置位听起来很糟糕。并发的祸根是共享,这里你不必要地强制执行了大量无意义的共享。如果可以节省内存,让每个标志占据一个字的大小,或者更好的是整个高速缓存行(64字节)。 - Kerrek SB
@KerrekSB,所以您建议我使用一个布尔字符数组而不是一个字符,对吗? - ATaylor
我可能建议您使用非常稀疏的数组,以便每个元素都在64字节边界上对齐。这样,任何单个标志检查都不会与任何其他标志检查发生冲突。(这与您的问题无关,但这是一个相当重要的考虑因素。) - Kerrek SB
@KerrekSB,我认为你所建议的方法行不通。看一下清除操作。它同时清除了除一个位以外的所有位。这可能也需要在单个原子变量内进行原子操作。同步变量必须是共享的。 - Wandering Logic
@WanderingLogic:也许吧。如果需要原子清除,最好使用互斥锁(然后使用单个字节存储状态就可以了)。哪种设计最好取决于访问配置文件。 - Kerrek SB
@WanderingLogic 实际上,我只是通过将其与要清除的位的反掩码进行逻辑“与”来清除一个位。因此,如果我想要清除第4位,它将与“1111 0111”进行“AND”操作。 - ATaylor
2个回答

6
C++03(也不是C99)根本没有关于原子性的规定。在大多数平台上,赋值是原子的(=每个人都看到旧值或新值),但由于它没有同步(=任何人在看到其他更新的新值后可能仍然看到旧值),因此它是无用的。任何其他操作,如增量、设置位等,可能甚至不是原子的。
C++11定义了std::atomic template,它确保了原子性和同步,因此您需要使用它。Boost为大多数C++03编译器提供了兼容的实现,而gcc从4.2版本开始就已经内置支持,正在被C++11所需更高级的支持所取代

自很久以前起,Windows API 就拥有了"原子操作"。而 Unix 的替代方案在引入 gcc 的 __sync 函数之前需要使用汇编语言(几个库提供了这种功能)。


请原谅,这是否意味着除非我的编译器支持C++11(就我所知,BCB 6无法支持),否则我将被卡住?或者这是否意味着我仍然是“好的”,因为该操作是原子操作,尽管在查看汇编时它看起来像多个操作?此外,即使存在竞争条件,在这种情况下也不会对我造成伤害,因为即使在释放时将一个插槽读取为“已占用”,它也将在下一步中被使用。这样行得通吗? - ATaylor
不,你并没有被卡住。你可以使用一个更加更新的编译器。BCB6是2002年的产物,甚至在C++03标准发布之前。现在我们已经到了2013年,C++11已经发布,第一批编译器已经具备了完整的功能,而C++14社区草案也已经出来了。 - Arne Mertz
@ATaylor:BCB 6 是特定于 Windows 的。因此,只需使用 Interlocked* 函数即可。 - Jan Hudec
@ArneMertz 嘿,是的,如果项目允许的话我可以这么做。(它已经存在了相当长的时间,我们还没有成功迁移。)但我会向我的上级提出建议。 - ATaylor
@JanHudec Interlocked 函数不仅适用于 .NET 应用程序吗?我还在查看页面。 - ATaylor
@ATaylor:不,它们自古以来就可以本地使用了。我最初链接了错误的参考文献,然后进行了更换。请使用当前的链接。 - Jan Hudec

1
访问共享数据时,存在三个潜在问题。首先,在需要多个总线周期的内存访问中可能会发生线程切换,这称为“撕裂”。其次,每个处理器都有自己的内存缓存,并且将数据写入一个缓存不会自动写入其他缓存,因此不同的线程可能会看到旧数据。第三,编译器可以移动指令,因此另一个处理器可能会看到对一个变量的后续存储,而没有看到对另一个变量的前置存储。
使用unsigned char类型的变量几乎肯定会消除第一个问题,但对其他两个问题没有影响。
为避免所有三个问题,请使用C++11中的atomic<unsigned char>或您的编译器和操作系统提供的任何同步技术。

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