假设某个线程正在某个内存位置使用__sync_val_compare_and_swap来原子设置一个值。另一个线程直接修改此内存位置而不进行比较和交换操作。在这种情况下(每个线程?)的一致性保证是什么?
答案 0 :(得分:0)
确切的行为因架构而异。您尚未指定正在运行的处理器,并且__sync_val_compare_and_swap
是否作为处理器上的指令(如lock cmpxchg
)实现,或者通过LL / SC指令可能对观察到的情况有影响行为(虽然我只提出LL / SC,因为它比CAS更敏感。)
此外,内存排序保证会改变行为:像ARM或Power这样的系统可能表现出与x86(-64)或SPARC不同的行为。我建议阅读Samy Al Bahra's ACM Queue article "Nonblocking Algorithms and Scalable Multicore Programming"以获取有关这可能如何影响程序行为的更多信息,不过我会稍微讨论一下。
您还没有说明您在CAS操作中究竟做了什么。这只是作业吗?您是否使用CAS循环实现了一些无锁数据结构?目前还不清楚,可以用更好的问题提供更好的答案。我将尽可能少地假设真正做出值得讨论的事情,所以让我们假设我们实际上在两个线程中都进行了读 - 修改 - 写,我们正试图增加。如果我们有以下内容:
volatile uint32_t i = 0;
bool stop = false;
void *
t0(void *arg __attribute__((unused)))
{
volatile uint32_t snap;
while (!stop) {
snap = i;
__sync_val_compare_and_swap(&i, snap, snap + 1);
}
return NULL;
}
void *
t1(void *arg __attribute__((unused)))
{
while (!stop) {
i++;
}
return NULL
}
我们不能谈论一致性保证,因为没有任何东西可以与之保持一致。 这个程序不一致。我们也不能说一个线程中的一致性保证,虽然很明显有时候t0
将无法增加该值。
这个例子并不是那么无害。即使处理器内在增加存储在内存中的数字(假设它不在缓存中),我们也不得不从内存中读取该数字,增加数字,然后将该数字写回。我们的t1
将始终成功递增一个值,但有时只会增加t0
的值。
为什么?
增量是读取/修改/写入操作,CAS也是如此。 i++
转换为inc (%rax)
并分解为从内存中读取,递增,写入内存,或者它是否直接转换为这些指令,何时以及如何读取和写入内存因架构而异。
CAS和增量都可能成功。 t0
运行的核心可能还没有观察到t1
对内存的写入,特别是在ARM或Power等宽松的内存排序体系结构上。从理论上讲,这可能导致t0
更新为t1
写入i
后面的值。
根据GCC文档,__sync_val_compare_and_swap
意味着完整的内存障碍,但是其他线程没有提供这样的障碍。
我们可以通过添加内存栅栏来解决这个问题:
void *
t1(void *arg __attribute__((unused)))
{
while (!stop) {
i++;
__sync_synchronize();
}
return NULL
}
这有帮助,因为我们现在保证由于__sync_synchronize
放置完整的内存屏障而导致内存操作的总排序。我们仍然可以在t0
和t1
之间进行比赛,但是如果他们这样做,我们将只会失去一个增量,我们永远不会“落后”,因为我们没有围栏。总而言之,仍然没有一致性保证。现在保证的是,如果两个线程都运行,则至少发生1次增量。
这是否一致或正确取决于您如何定义一致或正确。它肯定比以前更“正确”和“更一致”。
当然,如果我们想要真正的正确性,我们需要两个线程都使用__sync_val_compare_and_swap
。因为GCC插入了内存屏障,所以无论目标体系结构上的内存排序语义如何,这都确实保证了正确性。
最后,如果您正在撰写此类代码,我建议您使用Concurrency Kit。它的API更加令人愉悦和全面,它并不意味着任何地方都存在内存障碍(当不需要时可以产生积极的性能影响,使代码更加明确,并允许使用更轻量级的负载 - 或尽可能存储围栏。