根据CERT编码规则POS49-C访问共享结构中的相邻成员时的竞争条件?

根据CERT编码规则POS49-C ,访问相同结构的不同字段的不同线程可能会发生冲突。

我使用常规的unsigned int而不是bit-field。

struct multi_threaded_flags { unsigned int flag1; unsigned int flag2; }; struct multi_threaded_flags flags; void thread1(void) { flags.flag1 = 1; } void thread2(void) { flags.flag2 = 2; } 

我可以看到,即使是unsigned int,仍然存在竞争条件IF编译器决定使用加载/存储8个字节而不是4个字节。 我认为编译器永远不会这样做,并且竞争条件永远不会发生在这里,但这完全是我的猜测。

是否有关于此案例的明确定义的汇编/编译器文档? 我希望锁定,这是昂贵的,是这种情况恰好未定义的最后手段。

仅供参考,我使用gcc。

C11内存模型保证对不同结构成员(不是位字段的一部分)的访问是独立的,因此从不同的线程修改两个标志(即,加载8个字节,修改4,并回写8“不允许的场景”。

这种保证一般不会扩展到位域,所以你必须要小心。

当然,如果您同时从多个线程修改相同的标志,您可能会触发禁止数据争用,所以不要这样做。

在C11之前,ISO C没有什么可说的线程,编写multithreading代码依赖于其他标准(例如POSIX定义了pthreads的内存模型),而multithreading代码本质上取决于实际编译器的工作方式。

请注意,此CERT编码标准规则位于POSIX部分,并且似乎是关于没有 C11的pthreads。 (有一个CON32-C。当从 C11的multithreading规则访问位字段时防止数据争用 ,它们通过简单地将位字段提升为unsigned char来解决位字段并发问题,C11将其定义为“单独的存储位置” “这条规则似乎是一份经过充分编辑的副本,因为它的许多建议都很糟糕。”

但不幸的是,POSIX pthreads 没有明确定义“内存位置”是什么,这就是他们在这个主题上所要说的全部内容:

单个UNIX®规范,版本4,2016版( 在线HTML副本,需要免费注册 )

4.12内存同步

应用程序应确保限制多个控制线程(线程或进程)对任何内存位置的访问,以便没有控制线程可以读取或修改内存位置,而另一个控制线程可能正在修改它。 使用同步线程执行的函数以及相对于其他线程同步存储器来限制这种访问。

这就是C11更清楚地定义它的原因,其中只有位域从不同的线程写入是危险的(除了编译器错误)。

但是,我认为每个人(包括所有编译器)都同意单独的int变量/ struct members / array元素是独立的“内存位置”。 大多数真实世界的软件对intchar变量没有采取任何特殊的预防措施,这些变量可能由不同的线程(特别是在结构体之外)写入。

获取int错误的编译器将导致整个地方出现问题,除非错误仅限于非常具体的情况。 这样的大多数错误都很难通过测试来检测,因为通常非primefaces加载和存储的其他数据不会经常/永远地被另一个线程写入。 但是如果编译器总是为每个int做到这一点,那么问题会很快出现在某些软件中。

通常,单独的char成员也将被视为单独的“内存位置”,但某些C11之前的实现可能会对该规则有例外。 (特别是早期的Alpha AXP ,其中没有字节存储指令 (因此C11实现必须使用32位char ),但更新多个成员时发明写入的优化可能发生在任何地方,无论是偶然还是因为编译器开发人员定义“内存位置”为32或64位字。)

还有编译器错误的问题。 这甚至会影响打算符合C11的编译器 。 例如gcc bug 52080影响了一些非x86架构。 (2012年在gcc4.7中发现,几个月后修复为gcc4.8)。 使用位域“欺骗”编译器对包含64位字进行非primefaces读 – 修改 – 写操作,即使它包含非位域成员。 (Bitfields是编译器错误的诱饵。任何防御/安全编码标准都应该建议在不同成员可以从不同线程修改的结构中避免它们。并且绝对不要将它们放在实际锁定旁边。)

Herb Sutter的演讲atomic<>武器:C ++内存模型和现代硬件第2部分详细介绍了影响multithreading代码的编译器错误种类。 如果您使用的是现代编译器,那么现在(2017年)大部分都应该被淘汰。 发明写入(或相同值的非primefaces读取和写回)等大多数事情在C11之前通常仍被视为错误; C11大多刚刚确定了编制者已经试图遵循的规则。 它还可以更容易地报告此类错误,因为您可以毫不含糊地说它违反了标准,而不仅仅是“它破坏了我的代码”。


编码规则文章编写得很糟糕 。 其相邻位域的示例是不安全的,但它声称所有变量都存在风险。 一般情况下都不是这样,特别是C11没有。 许多pthread用户可以或已经使用C11编译器进行编译。

(我所指的措辞是“比特字段特别容易出现这种行为”,这错误地暗示允许结构的普通成员或碰巧在结构外部相邻的变量)

它是防御性编码标准的一部分,但它绝对应该区分标准需要什么,以及只是腰带和吊带防御编译器错误。


此外,将通常由不同线程访问的变量放入一个struct中通常很糟糕 。 虚假共享缓存行(通常为64字节)对性能非常不利,导致缓存未命中和(在无序x86 CPU上)内存排序错误推测(如需要回滚的分支错误预测)。单独使用的共享变量与位字段相同的字节更糟糕,因为它阻止了有效存储(任何存储必须是包含字节的RMW)。

通过将两个位字段提升为unsigned char来解决位字段问题比使用互斥锁更有意义,如果它们需要可以从不同的线程中独立写入。 如果你是偏执狂,甚至是无条件的。

如果两个成员经常一起使用,将它们放在附近是有意义的。 但是如果你要填充两个成员(就像那篇文章那样),你最好还是让它们至少是unsigned charbool而不是1字节的bitfields。

虽然老实说,有两个线程同时修改结构的单独成员似乎是糟糕的设计,除非其中一个成员锁定,并且修改是尝试获取锁定的一部分。 使用位字段作为锁定是一个坏主意,除非您正在使用类似x86的lock bts指令来对特定的ISA构建和您自己的锁定原语进行primefaces测试和设置。 即便如此,除非您需要将其与其他位域一起打包以节省空间,否则这是一个坏主意; 使用int lock:1暴露gcc bug的Linux代码int lock:1成员是一个可怕的想法。

此外,标志声明为volatile以确保编译器不会尝试在互斥锁之外移动它们。

如果你的编译器需要这个,你的编译器就会被严重破坏,并且会为大多数multithreading程序创建破坏的代码。 (除非编译器错误仅发生在位字段中,因为共享位字段很少见)。

大多数代码都不会使共享变量变得volatile ,并且依赖于互斥锁定/解锁阻止操作在编译或运行时从关键部分重新排序的保证。


早在2012年,可能还是今天, gcc -pthread可能会影响C89 / C99模式下的代码选择( -std=gnu99 )。 在关于该gcc bug的LWN文章的讨论中, 该用户声称 -pthread会在修改32位变量时禁止编译器执行64位加载/存储,但是如果没有-pthread它可以这样做(尽管在大多数架构,IDK为什么会这样)。 但事实certificate,即使使用-pthread ,gcc bug也会出现,所以它实际上是一个bug,而不是一个积极的优化选择。


ISO C11标准:

N1570,第3.14节定义 :

  1. 内存位置 :标量类型的对象,或者具有非零宽度的相邻位域的最大序列

  2. 注1:两个执行线程可以更新和访问单独的存储器位置,而不会相互干扰。

  3. 位字段和相邻的非位字段成员位于单独的存储器位置中。 …如果在它们之间声明的所有成员也是(非零长度)位域,同时更新同一结构中的两个非primefaces位域是不安全的,无论这些位置的大小是多少 – 田野恰好是。
  4. (…给出了一个带有位字段的结构的例子……)

因此在C11中,在编写一个位字段时,你不能假设编译器重写其他位域,否则你就是安全的。 除非使用separator :0字段强制编译器填充(或使用primefaces位操作),以便它可以更新您的位字段而不会出现其他字段的并发问题。 但是如果你想要安全,那么在多个线程一次写入的结构中使用位字段可能不是一个好主意。

另请参阅C11标准中的其他注释,例如由@Fuz链接的注释: 是否修改了一个元素,而另一个线程修改了同一个数组的另一个元素? 明确表示不允许编译器转换会造成这种危险。

这里要考虑的不仅仅是flag1和flag2会受到多个线程的影响。

声明结构时,您正在创建结构成员的关系或分组。 这意味着假定成员之间存在某种关系。

如果我在非线程安全版本上执行代码审查,我会标记此代码。 具有多年经验的开发人员更喜欢线程安全版本,因此不需要更深入地分析成员如何受multithreading影响,并且未来的代码更改不会引入与线程相关的错误。

示例:未来的开发人员决定优化您的内存空间示例,并将flag1和flag2更改为字节(8位内存值)。 现在flag1和flag2之间存在相互作用。 然后可能存在一个新的难以发现的bug。

从技术上讲,可能不需要使您的示例线程安全。 但是,使其成为线程安全的努力比代码审查更简单/更容易,以确定它是安全的。