首页 > 解决方案 > 并发模型 C++

问题描述

假设您得到以下代码:

class FooBar {
  public void foo() {
    for (int i = 0; i < n; i++) {
      print("foo");
    }
  }

  public void bar() {
    for (int i = 0; i < n; i++) {
      print("bar");
    }
  }
}

相同的实例FooBar将被传递给两个不同的线程。线程 A 将调用foo(),而线程 B 将调用bar(). 修改给定程序以输出“foobar” n 次。

对于leetcode上的以下问题,我们必须编写两个函数

void foo(function<void()> printFoo);
void bar(function<void()> printBar);

其中printFoo和相应printBar地是一个打印 Foo 的函数指针。函数foobar正在多线程环境中被调用,并且没有关于如何调用foobar被调用的顺序保证。

我的解决方案是

class FooBar {
private:
    int n;
    mutex m1;
    condition_variable cv;
    condition_variable cv2;
    bool flag;
public:
    FooBar(int n) {
        this->n = n;
        flag=false;
    }

    void foo(function<void()> printFoo) {

        for (int i = 0; i < n; i++) {
            unique_lock<mutex> lck(m1);
            cv.wait(lck,[&]{return !flag;});
            printFoo();
            flag=true;
            lck.unlock();
            cv2.notify_one();
        }
    }

    void bar(function<void()> printBar) {

        for (int i = 0; i < n; i++) {

            unique_lock<mutex> lck(m1);
            cv2.wait(lck,[&]{return flag;});
            printBar();
            flag=false;
            lck.unlock();
            cv.notify_one();

            // printBar() outputs "bar". Do not change or remove this line.

        }
    }
};

让我们假设,在t = 0调用 bar 时,然后在t = 10调用 foo 时, foo 穿过由 mutex 保护的临界区m1

我的问题是

由于 fencing 属性的 C++ 内存模型是否保证当bar函数从等待中恢复cv2时 flag 的值将设置为 true?

我是否正确假设线程之间共享的锁强制执行之前和之后的关系,如 Leslie Lamports 时钟系统的方式所示。编译器和 C++ 保证临界区(这里是锁的结束)结束之前的所有内容都将被任何租用锁的线程观察到,因此可以将常见的锁、原子、信号量可视化为前后执行通过在多线程环境中建立时间来行为。

我们可以只使用一个条件变量来解决这个问题吗?

有没有办法在不使用锁而只使用原子的情况下做到这一点。原子对锁有哪些性能改进?

如果我这样做cv.notify_one()并且相应cv2.notify_one()地在关键区域内会发生什么,是否有可能错过中断。

原始问题 https://leetcode.com/problems/print-foobar-alternately/

Leslie Lamports 论文 https://lamport.azurewebsites.net/pubs/time-clocks.pdf

标签: c++multithreading

解决方案


由于 fencing 属性,C++ 内存模型是否保证当 bar 函数从等待 cv2 恢复时 flag 的值将设置为 true?

就其本身而言,条件变量容易产生虚假唤醒。没有谓词子句的CV.wait(lck)调用可能由于各种原因返回。这就是为什么在进入之前在 while 循环中检查谓词条件总是很重要的原因wait。你永远不应该假设当wait(lck)返回时你正在等待的事情已经发生了。但是使用您在等待中添加的子句:cv2.wait(lck,[&]{return flag;});此检查会为您处理。所以是的,当wait(lck, predicate)返回时,那 flag将是真的。

我们可以只使用一个条件变量来解决这个问题吗?

绝对地。只需摆脱cv2并让两个线程在第一个cv.

有没有办法在不使用锁而只使用原子的情况下做到这一点。原子对锁有哪些性能改进?

当您可以在一个线程上轮询而不是等待时,原子是很棒的。想象一个 UI 线程想要向您显示汽车的当前速度。speed它会在每次帧刷新时轮询变量。但是另一个线程,“引擎线程”正在atomic<int> speed为轮胎的每次旋转设置该变量。这就是它的亮点——当你已经有了一个轮询循环,并且在 x86 上,原子操作主要是用 LOCK 操作码前缀实现的(例如并发由 CPU 正确完成)。

至于只针对锁和原子的实现......好吧,对我来说已经晚了。简单的解决方案,两个线程都只是睡眠并轮询一个原子整数,该整数随着每个线程的轮次而递增。每个线程只是等待 value 为“last+2”并每隔几毫秒轮询一次。效率不高,但会奏效。

对我来说,关于如何使用单个或一对互斥锁来做到这一点已经有点晚了。

如果我在关键区域内执行 cv.notify_one() 和对应的 cv2.notify_one() 会发生什么,是否有可能错过中断。

不,你很好。只要您的所有线程都持有锁并在进入wait调用之前检查它们的谓词条件。您可以notify在关键区域内部或外部进行调用。我总是建议做notify_allover notify_one,但这甚至可能是不必要的。


推荐阅读