加载-链接/存储-排他范式的想法是,如果在加载之后很快跟着存储,并且没有任何中间的内存操作,并且如果没有其他东西触摸到该位置,则存储有可能成功,但如果其他东西已经触碰了该位置,则存储肯定失败。不能保证存储不会因为无法解释的原因而失败;然而,如果在负载和存储之间的时间尽量缩短,并且它们之间没有内存访问,那么像以下循环一样:
do
{
new_value = __LDREXW(dest) + 1;
} while (__STREXW(new_value, dest));
通常情况下,可以在几次尝试内成功。如果根据旧值计算新值需要进行一些重要的计算,则应将循环重写为:
可以一般依赖于在几次尝试之后成功。如果基于旧值计算新值需要进行一些显著的计算,则应该将循环重写为:
do
{
old_value = *dest;
new_value = complicated_function(old_value);
} while (CompareAndStore(dest, new_value, old_value) != 0);
... Assuming CompareAndStore is something like:
uint32_t CompareAndStore(uint32_t *dest, uint32_t new_value, uint_32 old_value)
{
do
{
if (__LDREXW(dest) != old_value) return 1;
} while(__STREXW(new_value, dest);
return 0;
}
此代码将在计算新值时,如果 *dest 发生变化,则必须重新运行其主循环,但如果 __STREXW 由于其他原因失败 [希望这种情况不太可能发生,因为 __LDREXW 和 __STREXW 之间只会有约两个指令],则只需要重新运行小循环。
补充
“基于旧值计算新值”的一个复杂情况的例子是,当“值”实际上是对复杂数据结构的引用时。代码可能会获取旧引用,从旧引用中派生出一个新的数据结构,然后更新引用。这种模式在垃圾回收框架中经常出现,而在“裸机”编程中比较少见,但即使在编程裸机时,也有各种方式可以出现。通常的 malloc/calloc 分配器不是线程安全/中断安全的,但用于固定大小结构的分配器通常是安全的。如果有某个2的幂次方数量的数据结构(如255)的“池”,则可以使用类似以下的东西:
#define FOO_POOL_SIZE_SHIFT 8
#define FOO_POOL_SIZE (1 << FOO_POOL_SIZE_SHIFT)
#define FOO_POOL_SIZE_MASK (FOO_POOL_SIZE-1)
void do_update(void)
{
uint32_t new_thing = foo_pool_alloc();
uint32_t old_thing;
do
{
old_thing = foo_current_thing;
update_thing(&foo_pool[new_thing & FOO_POOL_SIZE_MASK],
&foo_pool[old_thing & FOO_POOL_SIZE_MASK);
} while(CompareAndSwap(&foo_current_thing, new_thing, old_thing) != 0);
foo_pool_free(old_thing);
}
如果不会经常有多个线程/中断/其他尝试同时更新同一事物,这种方法应该可以安全地执行更新。 如果可能尝试更新相同的项目之间存在优先关系,则最高优先级的任务将保证在第一次尝试时成功,其次高优先级的任务将在未被最高优先级任务抢占的任何尝试中成功,依此类推。 如果使用锁定,想要执行更新的最高优先级任务将不得不等待较低优先级的更新完成; 使用CompareAndSwap范式,最高优先级的任务将不受较低优先级的影响(但会导致较低优先级任务需要做无用功)。
__sync_add_and_fetch(address, 1)
使用这些操作生成了最优代码。就像这样:top:
ldaxr w1,[x0]
add w1,w1,#0x1
stlxr w2,w1,[x0]
cbnz w2,top
(对于混乱的格式表示抱歉)。 - ahcox