想象一个释放共享指针的代码:
auto tmp = &(the_ptr->a);
*tmp = 10;
the_ptr.dec_ref();
如果 dec_ref() 没有“释放”语义,编译器(或 CPU)可以将 dec_ref() 之前的内容移动到它之后(例如):
auto tmp = &(the_ptr->a);
the_ptr.dec_ref();
*tmp = 10;
这是不安全的,因为 dec_ref() 也可以同时从其他线程调用并删除对象。因此,在 dec_ref() 之前,它必须有一个“释放”语义才能留在那里。
现在让我们想象对象的析构函数如下所示:
~object() {
auto xxx = a;
printf("%i\n", xxx);
}
此外,我们将对示例进行一些修改,并将有 2 个线程:
// thread 1
auto tmp = &(the_ptr->a);
*tmp = 10;
the_ptr.dec_ref();
// thread 2
the_ptr.dec_ref();
然后,“聚合”代码将如下所示:
// thread 1
auto tmp = &(the_ptr->a);
*tmp = 10;
{ // the_ptr.dec_ref();
if (0 == atomic_sub(...)) {
{ //~object()
auto xxx = a;
printf("%i\n", xxx);
}
}
}
// thread 2
{ // the_ptr.dec_ref();
if (0 == atomic_sub(...)) {
{ //~object()
auto xxx = a;
printf("%i\n", xxx);
}
}
}
但是,如果我们只有 atomic_sub() 的“释放”语义,则可以这样优化这段代码:
// thread 2
auto xxx = the_ptr->a; // "auto xxx = a;" from destructor moved here
{ // the_ptr.dec_ref();
if (0 == atomic_sub(...)) {
{ //~object()
printf("%i\n", xxx);
}
}
}
但是那样,析构函数不会总是打印“a”的最后一个值(这段代码不再是无竞争的)。这就是为什么我们还需要为 atomic_sub 获取语义(或者,严格来说,当计数器在递减后变为 0 时,我们需要一个获取屏障)。