以线程安全的方式使用回调设置类成员

时间:2017-02-10 05:36:07

标签: c++ multithreading callback c++14

我有以下类允许通过昂贵的计算设置值,我们可以假装计算在其他一些线程中异步发生。该类还有一个获取当前值的方法,如果需要可能同步进行该计算:

class Example {
  public:
    Example() : x_(0) {}
    void Set(int x) {
        SomeOtherFunc(x, callback_, false);
    }

    void Finished(int y) {
      x_ = y;
    }

    const int Get() {
      if (!x_) {
          SomeOtherFunc(1, callback_, true);
      }
      return x_;
    }

  private:
    int x_;
    std::function<void(int)> callback_ =
        std::bind(&Example::Finished, this, std::placeholders::_1);
};

让我们假装这个功能:

void SomeOtherFunc(int x, const std::function<void(int)>& func,
                   bool blocking) {
  // Do something expensive, in another thread, possibly blocking.
  func(x * 2);
}

这是我想要的方式:

Example e1, e2;
e1.Set(5);
// "10 2"
std::cout << e1.Get() << " " << e2.Get();

我关注的是以下情况:

Example e;
e.Set(10); // Takes a while
e.Get();   // Need the value now

我的问题:

  1. Example类是否是线程安全的?如果没有,怎么会这样呢?似乎锁可以工作,但可能有点矫枉过正。
  2. 由于SomeOtherFunc中的工作很昂贵,我只想调用一次该函数。在调用之前将类中的标志设置为true,并在调用之前检查,这似乎是合理的(但它是否是线程安全的?)。问题是,如果我之后立即致电Set然后Get,我希望Get返回&#34;最终&#34; x_的值。我该如何确保?也就是说,如果设置了该标志,如何让Get等待回调以线程安全的方式完成?

2 个答案:

答案 0 :(得分:1)

您可能想要使用future

  

类模板std :: future提供了一种访问异步操作结果的机制:   异步操作(通过std :: async,std :: packaged_task或std :: promise创建)可以为该异步操作的创建者提供std :: future对象。   然后,异步操作的创建者可以使用各种方法来查询,等待或从std :: future中提取值。如果异步操作尚未提供值,则这些方法可能会阻塞。   当异步操作准备好将结果发送给创建者时,它可以通过修改链接到创建者的std :: future的共享状态(例如std :: promise :: set_value)来实现。

答案 1 :(得分:1)

您的Example类不是线程安全的,因为可以同时修改整数x_,这可能导致未定义的行为(数据竞争)。 此外,您可以多次执行昂贵的计算,因为您在Get()中遇到竞争条件,方法是检查x_然后调用将设置它的函数。 您需要一种机制来保证x_的值只计算一次,同时完全线程安全(Set()Get()可以同时调用)。

标准库提供了一种精确解决该问题的机制call_once(),旨在实现一次性事件,同时在线程之间正确同步共享数据。 您可以像这样使用它:

#include <mutex>

class Example {
  public:
    ...

    void Set(int x) {
        std::call_once(flag, SomeOtherFunc, x, callback_, false);
    }

    ...

    const int Get() {
        std::call_once(flag, SomeOtherFunc, 1, callback_, true);
        return x_;
    }

  private:
    std::once_flag flag;
    ...
};

这也处理了Get()在回调处理结果时必须等待的情况。

请注意,您不再(并且必须)检查x_Get()的值,因为这将构成数据竞争(另一个线程可能正在更新x_同时)。

另请注意,直接调用回调Finished()并非线程安全。可能最好将其移至private:部分。