C ++在两个不同的变量上使用memory_order_relaxed

时间:2017-10-12 18:40:39

标签: c++ multithreading thread-safety atomic

在ThreadMethodOne中放宽变量valA和valB同步的最正确方法是什么(假设没有错误的高速缓存共享valA和valB)?似乎我不应该更改ThreadMethodOne以使用memory_order_relaxed来加载valA,因为编译器可以在valB.load之后移动valA.load,因为valB.load上的memory_order_acquire不保护valA.load之后的valA移动做出改变。似乎我不能在valB.load上使用memory_order_relaxed,因为它不再与ThreadMethodTwo中的fetch_add同步。交换物品并放松valA的负担会更好吗?

这是正确的改变吗?

nTotal += valB.load(std::memory_order_acquire);
nTotal += valA.load(std::memory_order_relaxed);

查看Compiler Explorer上的结果似乎表明,当使用memory_order_relaxed进行valA或valB时,ThreadMethodOne的代码生成相同,即使我不交换指令的顺序。我还看到ThreadMethodTwo中的memory_order_relaxed仍然编译为与memory_order_release相同。将memory_order_relaxed更改为以下行似乎使其成为非锁定添加&#val ;.(valA.load(std :: memory_order_relaxed)+ 1,std :: memory_order_relaxed);'但我不知道这是否更好。

完整计划:

#include <stdio.h>
#include <stdlib.h>
#include <thread>
#include <atomic>
#include <unistd.h>

bool bDone { false };
std::atomic_int valA {0};
std::atomic_int valB {0};

void ThreadMethodOne()
{
    while (!bDone)
    {
        int nTotal {0};
        nTotal += valA.load(std::memory_order_acquire);
        nTotal += valB.load(std::memory_order_acquire);
        printf("Thread total %d\n", nTotal);
    }
}

void ThreadMethodTwo()
{
    while (!bDone)
    {
        valA.fetch_add(1, std::memory_order_relaxed);
        valB.fetch_add(1, std::memory_order_release);
    }
}

int main()
{
    std::thread tOne(ThreadMethodOne);
    std::thread tTwo(ThreadMethodTwo);

    usleep(100000);
    bDone = true;

    tOne.join();
    tTwo.join();

    int nTotal = valA.load(std::memory_order_acquire);
    nTotal += valB.load(std::memory_order_acquire);
    printf("Completed total %d\n", nTotal);
}

更好的样本离开原始样本,因为它是在评论中写的那个

#include <stdio.h>
#include <stdlib.h>
#include <thread>
#include <atomic>
#include <unistd.h>

std::atomic_bool bDone { false };
std::atomic_int valA {0};
std::atomic_int valB {0};

void ThreadMethodOne()
{
    while (!bDone)
    {
        int nTotalA = valA.load(std::memory_order_acquire);
        int nTotalB = valB.load(std::memory_order_relaxed);
        printf("Thread total A: %d B: %d\n", nTotalA, nTotalB);
    }
}

void ThreadMethodTwo()
{
    while (!bDone)
    {
        valB.fetch_add(1, std::memory_order_relaxed);
        valA.fetch_add(1, std::memory_order_release);
    }
}

int main()
{
    std::thread tOne(ThreadMethodOne);
    std::thread tTwo(ThreadMethodTwo);

    usleep(100000);
    bDone = true;

    tOne.join();
    tTwo.join();

    int nTotalA = valA.load(std::memory_order_acquire);
    int nTotalB = valB.load(std::memory_order_relaxed);
    printf("Completed total A: %d B: %d\n", nTotalA, nTotalB);
}

1 个答案:

答案 0 :(得分:1)

清理完代码后,请参阅我的评论,我们会得到类似的内容,

#include <atomic>
#include <iostream>

std::atomic_int valA {0};
std::atomic_int valB {0};

void ThreadMethodOne()
{
    int nTotalA = valA.load(std::memory_order_acquire);
    int nTotalB = valB.load(std::memory_order_relaxed);
    std::cout << "Thread total A: " << nTotalA << " B: " << nTotalB << '\n';
}

void ThreadMethodTwo()
{
    valB.fetch_add(1, std::memory_order_relaxed);
    valA.fetch_add(1, std::memory_order_release);
}

int main()
{
    std::thread tOne(ThreadMethodOne);
    std::thread tTwo(ThreadMethodTwo);

    tOne.join();
    tTwo.join();

    int nTotalA = valA.load(std::memory_order_acquire);
    int nTotalB = valB.load(std::memory_order_relaxed);
    std::cout << "Completed total A: " << nTotalA << " B: " << nTotalB << '\n';
}

该计划的可能结果是:

Thread total A: 0 B: 0
Completed total A: 1 B: 1

Thread total A: 0 B: 1
Completed total A: 1 B: 1

Thread total A: 1 B: 1
Completed total A: 1 B: 1

它始终打印Completed total A: 1 B: 1的原因是线程2已连接并因此完成,每个变量加1,而线程1中的加载对此没有影响。

如果线程1在线程2之前运行并完整地完成,那么它显然将打印0 0,而如果线程2在线程1之前运行并完全完成,则线程1将打印1 1.注意如何执行memory_order_acquire加载在线程1中没有强制执行任何操作。它可以很容易地读取0的初始值。

如果线程同时或多或少地运行,则0 1的结果也非常简单:线程1可能执行其第一行,然后线程2执行其两行,最后线程1读取由线程2到valB(它不必因为它是放松的,但在这种情况下我们只得到0 0输出;但至少它可能会读取1,如果我们等待足够长的时间)

因此,唯一感兴趣的问题是:为什么我们看不到输出为0?

原因是如果线程1为valA读取值1,则必须是线程2写入的值。这里读取其值的写入是写入释放,而读取本身是读取获取。这会导致同步发生,导致在释放之前发生的线程2的每个副作用在读取释放后对线程1中的每个内存访问都可见。换句话说,如果我们读取valA == 1那么后续读取valB(放宽或不放松)将看到写入线程2的valB,因此总是看到1而从不看到0。

不幸的是我不能多说这个,因为你的问题很不清楚:我不知道你对结果的期望是什么,或者想要成为什么样的结果;所以我无法对内存要求说些什么。