没有像mutex_Lock这样的协调机制的线程

C++ - Threads without coordinating mechanism like mutex_Lock

本文关键字:协调 机制 线程 Lock mutex      更新时间:2023-10-16

两天前我参加了一个面试。被面试的人c++很好,但多线程不行。当他让我写一个多线程代码的两个线程,其中一个线程打印1,3,5,…还有其他的指纹2 4 6…但是,输出应该是1,2,3,4,5 ....因此,我给出了下面的代码(sudo code)

mutex_Lock LOCK;
int  last=2;
int last_Value = 0;
void function_Thread_1()
{
      while(1)
      {
          mutex_Lock(&LOCK);
          if(last == 2)
          {
              cout << ++last_Value << endl;
              last = 1;
          }
          mutex_Unlock(&LOCK);
      }
}
void function_Thread_2()
{
      while(1)
      {
          mutex_Lock(&LOCK);
          if(last == 1)
          {
              cout << ++last_Value << endl;
              last = 2;
          }
          mutex_Unlock(&LOCK);
      }
}
在这之后,他说"这些线程即使没有那些锁也能正常工作。"这些锁会降低效率。"我的观点是,如果没有锁,就会出现这样的情况:一个线程会检查(last == 1或2),同时另一个线程会尝试将值更改为2或1。所以,我的结论是,它将工作没有锁,但这不是一个正确的/标准的方式。现在,我想知道谁是正确的,在什么基础上?

如果没有锁,并发运行两个函数将是未定义的行为,因为在访问lastlast_Value时存在数据竞争,而且(尽管不会导致UB)打印将是不可预测的。

有了锁,程序本质上变成了单线程,并且可能比单纯的单线程代码慢。但这只是问题的本质(即生成序列化的事件序列)。

我想面试官可能考虑过使用原子变量。

std::atomic模板的每个实例化和完全特化都定义了一个原子类型。原子类型的对象是c++中唯一免于数据竞争的对象;也就是说,如果一个线程写入原子对象,而另一个线程从中读取,则行为是定义良好的。此外,对原子对象的访问可以建立线程间同步,并按照std::memory_order指定的顺序对非原子内存进行访问。

[源]

我的意思是你唯一应该改变的是删除锁并将last变量更改为std::atomic<int> last = 2;而不是int last = 2;

这样可以安全地并发访问last变量。


出于好奇,我编辑了一下你的代码,并在我的Windows机器上运行:

#include <iostream>
#include <atomic>
#include <thread>
#include <Windows.h>
std::atomic<int>    last=2;
std::atomic<int>    last_Value = 0;
std::atomic<bool>   running = true;
void function_Thread_1()
{
      while(running)
      {
          if(last == 2)
          {
              last_Value = last_Value + 1;
              std::cout << last_Value << std::endl;
              last = 1;
          }
      }
}
void function_Thread_2()
{
      while(running)
      {
          if(last == 1)
          {
              last_Value = last_Value + 1;
              std::cout << last_Value << std::endl;
              last = 2;
          }
      }
}
int main() 
{
    std::thread a(function_Thread_1);
    std::thread b(function_Thread_2);
    while(last_Value != 6){}//we want to print 1 to 6
    running = false;//inform threads we are about to stop
    a.join();
    b.join();//join
    while(!GetAsyncKeyState('Q')){}//wait for 'Q' press
    return 0;
}

,输出总是:

1
2
3
4
5
6

Ideone拒绝运行此代码(编译错误)..

编辑:但这是一个工作的linux版本:)(感谢很快)

面试官不知道他在说什么。没有锁,lastlast_value上都有竞争。例如,编译器可能会在last_value的打印和递增之前重新排序对last的赋值,这可能会导致另一个线程执行过时的数据。此外,您可能会得到交错输出,这意味着两个数字不会被换行分隔。

另一件可能出错的事情是,编译器可能决定不重新加载last和(不太重要的)last_value,因为它不能(安全地)在这些迭代之间改变(因为数据竞争在c++ 11标准中是非法的,并且在以前的标准中不承认)。这意味着面试官建议的代码实际上很有可能产生无限循环,导致什么都不做。

虽然可以在没有互斥的情况下使代码正确,但这绝对需要具有适当排序约束的原子操作(在if语句中加载last时对lastacquire赋值的释放语义)。

当然,由于有效地序列化了整个执行过程,您的解决方案确实降低了效率。然而,由于运行时几乎完全花费在流输出操作中,这几乎肯定是通过使用锁进行内部同步的,因此您的解决方案不会再降低效率了。在代码中等待锁实际上可能比忙碌地等待它更快,这取决于可用资源(使用原子的非锁定版本在单核机器上执行时绝对会崩溃)