指针解引用是原子操作吗?

18

假设我有一个指向整数的指针。

volatile int* commonPointer = new int();

我有多个线程解引用那个指针。

int blah = *commonPointer;

但是,有一个线程需要更改那个指针的地址:

int* temp = new int();
int* old = commonPointer;
InterlockedExchange(&commonPointer,temp);
delete old;

现在,先不考虑有些线程可能读取“旧”值,而有些线程可能读取“新”值,这在我的情况下不是问题。

是否存在这样一种情况:一个线程开始解引用指针,就在地址被删除时,它会得到一个异常?或者解引用是原子性的,所以不会发生这种情况?


5
“足够原子”? 根据标准,读取32位整数不能保证是原子性的,这由您的平台定义。实际上通常会是原子性的,但您可以自己进行检查。 - Ed S.
@EdS。读取32位整数是原子性的。但是解引用指针...我不太确定。它不止一次读取.. 它会读取指针的32位(或x64中的64位)地址,然后需要从该地址进行另一次读取。 - Yochai Timmer
@EdS。InterlockedExchange保证指针值的原子更改。而解引用至少需要2个汇编指令。 - Yochai Timmer
1
如果您正在交换指针值,则此函数将取代InterlockedExchange。另外,一个volatile指向int的指针(您在问题中想要的内容)和一个指向volatile int的指针(您在问题中拥有的内容)之间存在重要区别。 - Tony Delroy
“delete old”会创建竞争条件 - 它可能在另一个读取pre-exchange指针的线程访问指向的值之前完成。虽然指针位没问题,但必须确保指向的生命周期适合所有消费者。 - Tony Delroy
显示剩余4条评论
4个回答

22

在这种情况下,C++标准中没有保证原子性。

您必须使用互斥量来保护相关的代码区域:即使使用std::atomic也不足以提供原子访问指针的操作,因为它不包括解引用操作。


std::atomic会保证变量的值是原子性的,但它不能保证对该值进行解引用的过程也是线程安全的。 - Yochai Timmer
4
难道这不正是我所说的吗? ;) - syam
1
是的,不知道为什么我没有注意到那个 :) - Yochai Timmer

5
也许,C++11
atomic<shared_ptr<int> > 

适应您的需求。它可以防止旧值在至少有一个引用到该值有效之前消失。

atomic<shared_ptr<int> >  commonPointer;

// producer:
{
    shared_ptr<int> temp(new int);
    shared_ptr<int> old= atomic_exchange(&commonPointer, temp); 
    //...
};// destructor of "old" decrements reference counter for the old value


// reader:
{
    shared_ptr<int> current= atomic_load(&commonPointer);

    // the referent pointed by current will not be deleted 
    // until   current is alive (not destructed);
}

然而,原子共享指针的无锁实现相当复杂,因此库实现中可能会使用锁或自旋锁(即使在您的平台上有该实现)。


使用标准工具的好解决方案。但至少会有两个锁,一个用于原子操作,另一个用于 shared_ptr。最好自己创建一个单一的锁。 - Yochai Timmer
原子共享指针可以使用危险指针实现。虽然它很复杂,但可以在没有锁的情况下实现。 - Maciej Piechotka

3

首先,在您的声明中使用的volatile没有任何实际效果。其次,一旦您在一个线程中修改了一个值,并在多个线程中访问它,则所有访问都必须受到保护。否则,您将会遇到未定义行为。我不知道InterlockedExchange提供了哪些保证,但我确定它对不调用它的任何线程都没有影响。


InterlockedExchange确保指针中的值在所有CPU上以原子方式更新。但正如我在答案中所讨论的那样,它并没有帮助,因为我们仍然存在指针值和被解引用的值被读取之间的竞争。请参见我帖子顶部的Edit2。 - Mats Petersson

1
编辑2:抱歉,不会有帮助。您需要在访问周围使用互斥锁 - 编译器生成的代码可能(非常可能)将指针加载到寄存器[或其他存储器,例如堆栈,如果它是没有寄存器的处理器]中,然后访问指针指向的内存,并且与此同时,指针正在被另一个线程更新。保证指针正确的唯一方法是使用互斥锁或类似构造来关闭整个访问块。其他任何方法都可能失败。
正如syam所说,标准不能保证即使读取指针指向的32位值也是原子的 - 它取决于系统的实现。但是,如果您问“我会得到一个旧值或新值的值”,那么至少x86和x86-64将保证这一点。其他机器体系结构可能不会(在SMP 68000处理器上实现的32位int实现不会保证它,因为写入每次16位,并且第二个处理器可能已经写入了其中的一半,但我不知道曾经构建过带有68000处理器的SMP系统)。 InterlockedExchange(不是“标准”函数)将保证该线程的处理器具有指针本身的独占访问权,因此进行操作是安全的 - 在此时没有其他处理器能够访问指针。这就是x86体系结构中“锁定”指令的全部意义 - 它们是“安全”的(而且相当慢,但假设您不会每次都这样做...)。
编辑:请注意,您必须小心处理commonPointer本身,因为编译器可能不会意识到您正在使用另一个线程来更新它。因此,您仍然可能从旧的指针值中读取。
调用一个未被内联为无用代码的函数或声明指针volatile int * volatile commonPointer;应该解决问题。(引起人们对我的答案使用volatile而将其下降,因为“没有问题需要volatile”如之前有人发布的。)
[请参见上面的编辑2]

即使使用两个volatile也不足够,即使采用微软扩展的volatile含义(并非所有编译器都实现了,也不是所有编译器选项都支持)。在一般情况下,volatile是完全无关的。 - James Kanze

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接