31

假设对齐的指针加载和存储在目标平台上自然是原子的,这有什么区别:

// Case 1: Dumb pointer, manual fence
int* ptr;
// ...
std::atomic_thread_fence(std::memory_order_release);
ptr = new int(-4);

这:

// Case 2: atomic var, automatic fence
std::atomic<int*> ptr;
// ...
ptr.store(new int(-4), std::memory_order_release);

还有这个:

// Case 3: atomic var, manual fence
std::atomic<int*> ptr;
// ...
std::atomic_thread_fence(std::memory_order_release);
ptr.store(new int(-4), std::memory_order_relaxed);

我的印象是它们都是等价的,但是Relacy在第一种情况下检测到数据竞争(仅):

struct test_relacy_behaviour : public rl::test_suite<test_relacy_behaviour, 2>
{
    rl::var<std::string*> ptr;
    rl::var<int> data;

    void before()
    {
        ptr($) = nullptr;
        rl::atomic_thread_fence(rl::memory_order_seq_cst);
    }

    void thread(unsigned int id)
    {
        if (id == 0) {
            std::string* p  = new std::string("Hello");
            data($) = 42;
            rl::atomic_thread_fence(rl::memory_order_release);
            ptr($) = p;
        }
        else {
            std::string* p2 = ptr($);        // <-- Test fails here after the first thread completely finishes executing (no contention)
            rl::atomic_thread_fence(rl::memory_order_acquire);

            RL_ASSERT(!p2 || *p2 == "Hello" && data($) == 42);
        }
    }

    void after()
    {
        delete ptr($);
    }
};

我联系了 Relacy 的作者,以了解这是否是预期的行为;他说我的测试用例中确实存在数据竞争。但是,我很难发现它;有人可以向我指出比赛是什么吗?最重要的是,这三种情况有什么区别?

更新:我想到 Relacy 可能只是在抱怨跨线程访问的变量的原子性(或者说缺乏原子性)......毕竟,它不知道我打算只在平台上使用此代码其中对齐的整数/指针访问自然是原子的。

另一个更新:Jeff Preshing 写了一篇优秀的博客文章,解释了显式栅栏和内置栅栏(“栅栏”与“操作”)之间的区别。案例 2 和 3 显然不等价!(无论如何,在某些微妙的情况下。)

4

5 回答 5

14

我相信代码有种族。案例 1 和案例 2 不等价。

29.8 [atomics.fences]

-2-如果存在原子操作XY ,则释放栅栏A与获取栅栏B同步,两者都对某个原子对象M进行操作,使得A在X之前排序,X修改MY在B之前排序,并且Y读取由X写入的值或由假设的释放序列中的任何副作用写入的值,如果它是一个释放操作,X将开始。

在情况 1 中,您的发布栅栏与获取栅栏不同步,因为ptr它不是原子对象,并且存储和加载ptr不是原子操作。

案例 2 和案例 3 是等价的(实际上,不完全是,请参阅 LWimsey 的评论和答案),因为ptr是原子对象,而存储是原子操作。([atomic.fences] 的第 3 和第 4 段描述了栅栏如何与原子操作同步,反之亦然。)

栅栏的语义仅针对原子对象和原子操作进行定义。您的目标平台和您的实现是否提供更强的保证(例如将任何指针类型视为原子对象)充其量是实现定义的。

注意,对于案例 2 和案例 3,获取操作ptr可能发生在存储之前,因此会从未初始化的atomic<int*>. 简单地使用获取和释放操作(或栅栏)并不能确保存储发生在加载之前,它只能确保如果加载读取存储的值,那么代码是正确同步的。

于 2013-01-06T14:26:03.757 回答
14

几个相关的参考资料:

您和其他读者可能会对上述内容感兴趣。

于 2013-01-06T18:25:53.547 回答
13

尽管各种答案涵盖了潜在问题的部分内容和/或提供了有用的信息,但没有一个答案正确地描述了所有三种情况的潜在问题。

为了在线程之间同步内存操作,使用释放和获取屏障来指定排序。
在图中,线程 1 中的内存操作 A 不能向下移动越过(单向)释放屏障(不管这是对原子存储的释放操作,还是在放松的原子存储之后的独立释放栅栏)。因此,内存操作 A 保证在原子存储之前发生。线程 2 中的内存操作 B 也是如此,它不能向上移动越过获取屏障;因此原子负载发生在内存操作之前 B.

在此处输入图像描述

atomicptr本身基于它具有单个修改顺序的保证提供线程间顺序。一旦线程 2 看到 的值ptr,就可以保证存储(以及内存操作 A)发生在加载之前。因为保证加载发生在内存操作 B 之前,传递性规则说内存操作 A 发生在 B 之前并且同步完成。

有了这个,让我们看看你的3个案例。

案例 1 被破坏,因为ptr是一种非原子类型,在不同的线程中被修改。这是数据竞争的经典示例,它会导致未定义的行为。

案例 2 是正确的。作为参数,整数分配new在释放操作之前排序。这相当于:

// Case 2: atomic var, automatic fence
std::atomic<int*> ptr;
// ...
int *tmp = new int(-4);
ptr.store(tmp, std::memory_order_release);

案例 3 被打破了,尽管是以一种微妙的方式。问题是,即使ptr分配在独立栅栏之后正确排序,整数分配 ( new) 也在栅栏之后排序,导致整数内存位置上的数据竞争。

该代码相当于:

// Case 3: atomic var, manual fence
std::atomic<int*> ptr;
// ...
std::atomic_thread_fence(std::memory_order_release);

int *tmp = new int(-4);
ptr.store(tmp, std::memory_order_relaxed);

如果将其映射到上图,则该new运算符应该是内存操作 A 的一部分。在释放栅栏下方进行排序,排序保证不再成立,整数分配实际上可能与线程 2 中的内存操作 B 重新排序。因此,load()线程 2 中的 a 可能会返回垃圾或导致其他未定义的行为。

于 2017-04-15T17:38:52.557 回答
1

支持原子变量的内存只能用于原子的内容。然而,一个简单的变量,如案例 1 中的 ptr,是另一回事。一旦编译器有权写入它,它就可以向它写入任何内容,甚至是当您用完寄存器时的临时值的值。

请记住,您的示例在病态上是干净的。给出一个稍微复杂一点的例子:

std::string* p  = new std::string("Hello");
data($) = 42;
rl::atomic_thread_fence(rl::memory_order_release);
std::string* p2 = new std::string("Bye");
ptr($) = p;

编译器选择重用您的指针是完全合法的

std::string* p  = new std::string("Hello");
data($) = 42;
rl::atomic_thread_fence(rl::memory_order_release);
ptr($) = new std::string("Bye");
std::string* p2 = ptr($);
ptr($) = p;

为什么会这样做?我不知道,也许是一些奇特的技巧来保持高速缓存行之类的。关键是,由于 ptr 在情况 1 中不是原子的,因此在 'ptr($) = p' 行上的写入和 'std::string* p2 = ptr($)' 上的读取之间存在竞争情况,产生未定义的行为。在这个简单的测试用例中,编译器可能不会选择行使这个权利,这可能是安全的,但在更复杂的情况下,编译器有权随意滥用 ptr,而 Relacy 抓住了这一点。

我最喜欢的关于该主题的文章:http: //software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-c​​ould-possibly-go-wrong

于 2013-09-03T05:18:57.797 回答
0

第一个例子中的竞争是在指针的发布和它指向的东西之间。原因是,您在围栏之后创建和初始化了指针(= 与指针的发布在同一侧):

int* ptr;    //noop
std::atomic_thread_fence(std::memory_order_release);    //fence between noop and interesting stuff
ptr = new int(-4);    //object creation, initalization, and publication

如果我们假设 CPU 对正确对齐的指针的访问是原子的,则可以通过编写以下代码来更正代码:

int* ptr;    //noop
int* newPtr = new int(-4);    //object creation & initalization
std::atomic_thread_fence(std::memory_order_release);    //fence between initialization and publication
ptr = newPtr;    //publication

请注意,即使这在许多机器上都可以正常工作,但 C++ 标准中绝对不能保证最后一行的原子性。所以最好atomic<>首先使用变量。

于 2017-04-13T11:22:50.500 回答