5

两天前我参加了一次面试。受访者擅长 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);
      }
}

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

4

3 回答 3

3

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

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

于 2013-05-05T11:13:54.337 回答
3

我认为面试官可能已经考虑过使用原子变量。

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 版本 :) (感谢很快)

于 2013-05-05T11:42:59.797 回答
2

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

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

虽然可以在没有 mutice 的情况下使代码正确,但这绝对需要具有适当排序约束的原子操作(在 if 语句的分配lastacquire负载上的释放语义)。last

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

于 2013-05-05T11:17:58.877 回答