通过阅读该标准,
不支持*(_Atomic TYPE*)&(TYPE){0}
(换句话说,将指向非原子的指针转换为指向相应原子的指针并取消引用)。
如果TYPE
是无锁的,gcc和/或clang是否会将其识别为扩展? (问题1)
第二个相关问题:我的印象是,如果无法将TYPE
实现为无锁原子,则需要在相应的_Atomic TYPE
中嵌入锁。但是,如果我将TYPE
做成比较大的结构,则在clang
和gcc
上,它的大小都与_Atomic TYPE
相同。
两个问题的代码:
#include <stdatomic.h>
#include <stdio.h>
#if STRUCT
typedef struct {
int x;
char bytes[50];
} TYPE;
#else
typedef int TYPE;
#endif
TYPE x;
void f (_Atomic TYPE *X)
{
*X = (TYPE){0};
}
void use_f()
{
f((_Atomic TYPE*)(&x));
}
#include <stdio.h>
int main()
{
printf("%zu %zu\n", sizeof(TYPE), sizeof(_Atomic TYPE));
}
现在,如果我使用-DSTRUCT
编译上述代码段,则gcc和clang都将struct及其原子变体的大小保持相同,并且它们会生成对名为__atomic_store
的函数的调用商店(通过与-latomic
链接解决)。
如果在结构的_Atomic
版本中没有嵌入锁,该如何工作? (问题2)
答案 0 :(得分:4)
_Atomic
会在某些情况下更改Clang上的对齐方式,将来GCC也可能会得到修复(PR 65146)。在这些情况下,通过强制转换添加_Atomic
无效(从C标准的角度来看这很好,因为正如您所指出的那样,它是未定义的行为)。
如果对齐方式正确,则更适合使用__atomic
内置函数,这些内置函数正是为此用例而设计的:
如上所述,如果ABI对普通(非原子)类型的对齐方式不足,并且_Atomic
会更改对齐方式(目前仅使用Clang),则此方法将无效。
这些内建函数在非原子类型的情况下也可以使用,因为它们使用行外锁。这也是为什么使用相同机制的_Atomic
类型不需要额外存储的原因。这意味着由于无意间共享锁,存在一些不必要的争用。这些锁的实现方式是一个实现细节,在未来的libatomic
版本中可能会更改。
通常,对于具有涉及锁定的原子内建函数的类型,无法将它们与共享或别名内存映射一起使用。这些内建函数也不是异步信号安全的。 (无论如何,所有这些功能在技术上都超出了C标准。)
答案 1 :(得分:0)
此方法不是合法的C11,但是我设法使编译器(英特尔2019)欺骗如下,在原子类型和非原子“简单”类型之间进行强制转换。
首先,我在系统(x86_64)上的stdatomic.h内部进行了查看,以了解各种原子类型的实际定义是什么。据我所知,简单的整数类型和指针的原子类型与普通类型相同,而且它们明确地是“无锁的”。
下一步是使用sizeof()运算符查看原子类型实际使用了多少个字节,我再次发现一个原子int是4个字节,一个原子指针是8个-正如我所期望的在64位上系统。
编译器禁止显式强制转换,但是这样做有效:
typedef struct { void *ptr; } IS_NORMAL;
typedef struct { atomic_address ptr; } IS_ATOMIC;
IS_NORMAL a;
IS_ATOMIC *b = (IS_ATOMIC *)&a;
a.ptr = <address>
/* then inspection in the debugger shows that b->ptr is also <address> */
这将很乐意让我在上述两种结构类型之间进行转换,并且当我在IS_ATOMIC指针变量上使用原子函数(例如atomic_exchange())时,调试器向我显示了非原子结构地址的内容更改为期望值。
这时您可能会问“为什么这样做?”答案是,我有一个多线程应用程序,我想在其中锁定数据库记录很短的时间,以便一个线程可以更新它,而不会与其他线程争用,然后在完成后释放锁定。从历史上讲,我使用关键部分来保护此操作,但这是非常悲观的,因为我可能有-10,000,000条记录并随机更新它们,因此两个线程实际尝试更新同一条记录的机会非常小,但是关键部分无条件地阻塞所有线程。每个记录都由一个指针引用,因此过程:
因此,步骤(1)锁定而步骤(4)解锁,与关键部分方法不同,访问只有在两个线程试图访问 相同 地址。它似乎可以正常工作,并且在我的6核心系统(超线程,因此有12个线程)上,与在实际数据集上使用单个关键部分相比,它的速度大约快5倍。
那么为什么不首先将指向记录的指针定义为原子?答案是,该特定代码可以在其他地方进行对该信息的无线程访问,并且还可以以一种已知的无竞争方式进行线程访问。实际上,在大多数情况下,由于成本原因,我不希望拥有锁定机制。时序测试表明,典型的原子锁定/解锁操作在我的系统上似乎要花费5到10纳秒,并且我想避免不需要的开销,因此在那种情况下,我只使用原始指针。 / p>
我将其作为解决此特定问题的方式。我知道这不是正确的C11,我知道它可能仅适用于x86类型的体系结构-或至少仅适用于整数和指针类型为无锁且“本质上是原子的”体系结构-我也接受可能更好的方法如果您知道如何用汇编器编写(我不知道),则可以锁定给定地址。我很高兴听到更好的解决方案。
偶然地,我也尝试了事务性内存(即_xbegin().. _xend())作为解决此问题的一种方法。我发现它可以解决一些小的测试问题,但是一旦将其扩展到实际数据时,我就会经常遇到_xbegin()失败,并且我认为这是因为当您访问的地址不在高速缓存中时,它往往会退出紧急状态,强迫您采用后备代码路径。英特尔不太了解其工作方式的详细信息,因此这种解释可能是错误的。
我还查看了硬件锁消除技术,作为加快关键部分方法的一种方法,但是据我所知,由于易受黑客攻击,它已被弃用..无论如何,我实在太厚了,无法理解如何使用它!