C ++ - 没有协调机制的线程,如mutex_Lock

时间:2013-05-05 11:02:55

标签: c++ multithreading mutex

两天前我参加了一次面试。受访的人在C ++方面表现很好,但在多线程方面则不然。当他让我为两个线程的多线程编写代码时,其中一个线程打印1,3,5,...和另一个打印2,4,6,...但是,输出应该是1,2,3,4,5,....所以,我给出了下面的代码(sudo代码)

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);
      }
}

在此之后,他说“即使没有这些锁,这些线程也能正常工作。这些锁会降低效率”。我的观点是没有锁定会有一个线程将检查(最后== 1或2)同时另一个线程将尝试将值更改为2或1的情况。所以,我的结论是它将没有锁定,但这不是一个正确/标准的方式。现在,我想知道谁是正确的,在哪个基础上?

3 个答案:

答案 0 :(得分:3)

如果没有锁定,同时运行这两个函数将是未定义的行为,因为在lastlast_Value的访问中存在数据竞争此外(虽然不会导致UB)打印将是不可预测的。< / p>

使用锁定,程序基本上是单线程的,并且可能比天真的单线程代码更慢。但这仅仅是问题的本质(即产生序列化的事件序列)。

答案 1 :(得分:3)

我认为采访者可能已经考虑过使用原子变量。

  

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

[Source]

我的意思是你应该更改的唯一内容是删除锁定并将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拒绝运行此代码(编译错误)..

编辑:但here是一个有效的Linux版本:)(很快就会感谢)

答案 2 :(得分:2)

面试官不知道他在说什么。如果没有锁定,您将在lastlast_value上进行比赛。例如,编译器可以在last的打印和增量之前将赋值重新排序为last_value,这可能导致另一个线程在陈旧数据上执行。此外,您可以获得交错输出,这意味着两个数字之类的东西不会被换行分隔。

另一件可能出错的事情是编译器可能决定不重新加载last和(不太重要)last_value每次迭代,因为它无法(安全地)在这些迭代之间进行更改(因为数据竞争在C ++ 11标准中是非法的,并且在以前的标准中没有得到承认)。这意味着面试官建议的代码实际上很有可能创造无限循环,无所事事。

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

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