C ++ 11中的锁是否可以确保所访问数据的新鲜度?

时间:2018-10-11 14:14:07

标签: c++ multithreading c++11 concurrency parallel-processing

通常,当使用由多个线程同时访问的std :: atomic类型时,不能保证线程在访问它们时将读取“最新”值,并且线程可能会从缓存中获取陈旧值或任何更旧的值。获取最新值的唯一方法是诸如compare_exchange_XXX之类的函数。 (请参阅问题herehere

#include <atomic>

std::atomic<int> cancel_work = 0;
std::mutex mutex;

//Thread 1 executes this function
void thread1_func() 
{
    cancel_work.store(1, <some memory order>);
}


// Thread 2 executes this function
void thread2_func() 
{
    //No guarantee tmp will be 1, even when thread1_func is executed first
   int tmp = cancel_work.load(<some memory order>); 
}

但是我的问题是,改用互斥锁和锁会发生什么?我们是否可以保证访问的共享数据的新鲜度?

例如,假设线程1和线程2同时运行,并且线程1首先获得锁(首先执行)。是否保证线程2将看到修改后的值而不是旧值? 在这种情况下,共享数据“ cancel_work”是否是原子的是否重要?

#include <atomic>

int cancel_work = 0;  //any difference if replaced with std::atomic<int> in this case?
std::mutex mutex;

// Thread 1 executes this function
void thread1_func() 
{
    //Assuming Thread 1 enters lock FIRST
    std::lock_guard<std::mutex> lock(mutex);

    cancel_work = 1;
}


// Thread 2 executes this function
void thread2_func() 
{
    std::lock_guard<std::mutex> lock(mutex);

    int tmp = cancel_work; //Will tmp be 1 or 0?
}

int main()
{
   std::thread t1(thread1_func);
   std::thread t2(thread2_func);

   t1.join(); t2.join();

   return 0;
}

2 个答案:

答案 0 :(得分:1)

是的,使用互斥锁/锁可以确保thread2_func()将获得修改后的值。

但是,根据std::atomic规范:

  

仅在释放线程之间建立同步   并获取相同的原子变量。其他线程可以看到   内存访问顺序与两个或两个不同   同步线程。

因此,您的代码也可以使用获取/释放逻辑正常工作。

#include <atomic>

std::atomic<int> cancel_work = 0;

void thread1_func() 
{
    cancel_work.store(1, std::memory_order_release);
}

void thread2_func() 
{
    // tmp will be 1, when thread1_func is executed first
    int tmp = cancel_work.load(std::memory_order_acquire); 
}

答案 1 :(得分:1)

C ++标准仅在格式良好的程序中约束抽象机的可观察行为,而在抽象机执行期间的任何地方都没有未定义的行为。

它不能保证程序执行的物理硬件操作与行为之间的映射。

在您的情况下,在抽象计算机上,thread1thread2的执行之间没有顺序。即使在thread1之前安排thread2进行调度和运行的物理硬件,这也会在程序生成的输出中放置零约束(在您的简单示例中)。程序的输出仅受抽象机可以产生的合法输出的约束。

C ++编译器可以合法:

  1. 完全消除您的程序,等同于返回0;

  2. 证明cancel_workthread2的读取相对于cancel_work以外的0的所有修改都是无序列的,并将其更改为恒定的读取0

  3. 实际上首先运行thread1然后运行thread2,但是证明它可以将thread2中的操作视作在thread1运行之前发生,所以不要不必费心在thread2中刷新缓存行并从cancel_work中读取陈旧的数据。

硬件上实际发生的情况不会影响程序可以合法执行的操作。程序可以合法地做的是在线程状态中,受抽象机的可观察行为以及同步原语的行为及其在不同线程中的使用的限制。

对于在发生关系之前发生的实际情况,您需要以下内容:

std::thread(thread1_func).join();
std::thread(thread2_func).join();

现在我们知道thread1_func中的所有内容都在thread2_func之前发生。

我们仍然可以将您的程序重写为return 0;和类似的更改。但是我们现在可以保证thread1_functhread2_func代码之前发生。

请注意,我们可以通过以下方法消除上述(1):

std::lock_guard<std::mutex> lock(mutex);

int tmp = cancel_work; //Will tmp be 1 or 0?
std::cout << tmp;

并使tmp实际被打印。

然后可以将该程序转换为可打印10且完全没有线程的程序。它可以保留线程,但是更改thread2_func以输出常量0。等等


因此我们将您的程序重写为如下形式:

std::condition_variable cv;
bool writ = false;
int cancel_work = 0;  //any difference if replaced with std::atomic<int> in this case?
std::mutex mutex;

// Thread 1 executes this function
void thread1_func() 
{
    {
      std::lock_guard<std::mutex> lock(mutex);

      cancel_work = 1;
    }
    {
      std::lock_guard<std::mutex> lock(mutex);
      writ = true;
      cv.notify_all();
    }
}


// Thread 2 executes this function
void thread2_func() 
{
    std::unique_lock<std::mutex> lock(mutex);

    cv.wait(lock, []{ return writ; } );

    int tmp = cancel_work;
    std::cout << tmp; // will print 1
}

int main()
{
   std::thread t1(thread1_func);
   std::thread t2(thread2_func);

   t1.join(); t2.join();

   return 0;
}

,现在thread2_func发生在thread1_func之后,一切都很好。 保证1