【问题标题】:Thread-safe initialization of atomic variable in C++C++中原子变量的线程安全初始化
【发布时间】:2012-04-18 12:42:09
【问题描述】:

考虑以下 C++11 代码,其中类 B 被实例化并由多个线程使用。因为B 修改了一个共享向量,所以我必须在B 的ctor 和成员函数foo 中锁定对它的访问。为了初始化成员变量id,我使用了一个作为原子变量的计数器,因为我从多个线程访问它。

struct A {
  A(size_t id, std::string const& sig) : id{id}, signature{sig} {}
private:
  size_t id;
  std::string signature;
};
namespace N {
  std::atomic<size_t> counter{0};
  typedef std::vector<A> As;
  std::vector<As> sharedResource;
  std::mutex barrier;

  struct B {
    B() : id(++counter) {
      std::lock_guard<std::mutex> lock(barrier);
      sharedResource.push_back(As{});
      sharedResource[id].push_back(A("B()", id));
    }
    void foo() {
      std::lock_guard<std::mutex> lock(barrier);
      sharedResource[id].push_back(A("foo()", id));
    }
  private:
    const size_t id;
  };
}

不幸的是,这段代码包含一个竞争条件并且不能像这样工作(有时 ctor 和 foo() 不使用相同的 id)。如果我将 id 的初始化移动到由互斥锁锁定的 ctor 主体,它可以工作:

struct B {
  B() {
    std::lock_guard<std::mutex> lock(barrier);
    id = ++counter; // counter does not have to be an atomic variable and id cannot be const anymore
    sharedResource.push_back(As{});
    sharedResource[id].push_back(A("B()", id));
  }
};

你能帮我理解为什么后一个例子有效(是因为它不使用相同的互斥锁吗?)?有没有一种安全的方法可以在 B 的初始化列表中初始化 id 而不会将其锁定在 ctor 的主体中?我的要求是id 必须是const,并且id 的初始化发生在初始化列表中。

【问题讨论】:

  • 您能否发布导致问题的实际代码。您提出的代码没有意义(至少在没有A 的定义的情况下)。例如,您不能简单地访问sharedResource[id],而无需实际调整sharedResource 的大小以包含id + 1 元素。除非A 包含成员函数push_back,否则代码甚至不应该编译。
  • @JamesKanze 为什么A 需要push_back 成员?我只看到一个 (const char*,size_t) 构造函数和一个移动/复制构造函数在使用中。 OP:如果可能,请将其设为SSCCE
  • @je4d : sharedResource 是一个std::vector&lt;A&gt;,所以sharedResource[id] 返回一个A&amp;sharedResource[id].push_back(...) 因此调用A::push_back
  • @ildjarn 啊,是的,我扫描它的速度太快了,并认为因为 A 被推送,它没有被推送到 A,因为这对push_back 的常规语义。我希望代码不是 OP 实际打算写的。
  • 使用 push_back 或按索引访问。两者放在一起没有任何意义。

标签: c++ thread-safety c++11 mutex atomic


【解决方案1】:

首先,发布的代码中仍然存在一个基本的逻辑问题。 您将++ counter 用作id。考虑B 的第一个创建, 在一个线程中。 B 将有 id == 1;在push_back 之后 sharedResource,您将拥有sharedResource.size() == 1,并且 访问它的唯一合法索引是0

此外,代码中有明确的竞争条件。即使你 纠正上述问题(用counter ++初始化id),假设 countersharedResource.size() 目前都是 0; 你刚刚初始化。线程一进入B的构造函数, 递增counter,所以:

counter == 1
sharedResource.size() == 0

然后它被线程 2 中断(在它获取互斥体之前),它 还增加 counter(到 2),并使用其先前的值 (1) 作为 id。然而,在线程 2 中的 push_back 之后,我们只有 sharedResource.size() == 1,唯一合法的索引是0。

在实践中,我会避​​免使用两个单独的变量(countersharedResource.size()) 应该具有相同的值。从 经验:应该相同的两件事不会是唯一的 应该使用的时间冗余信息是什么时候用于 控制;即在某些时候,你有一个assert( id == sharedResource.size() ),或类似的东西。我会使用类似的东西:

B::B()
{
    std::lock_guard<std::mutex> lock( barrier );
    id = sharedResource.size();
    sharedResource.push_back( As() );
    //  ...
}

或者如果你想使id const:

struct B
{
    static int getNewId()
    {
        std::lock_guard<std::mutex> lock( barrier );
        int results = sharedResource.size();
        sharedResource.push_back( As() );
        return results;
    }

    B::B() : id( getNewId() )
    {
        std::lock_guard<std::mutex> lock( barrier );
        //  ...
    }
};

(请注意,这需要两次获取互斥锁。或者,您 可以传递完成更新所需的附加信息 sharedResourcegetNewId(),让它完成整个工作。)

【讨论】:

    【解决方案2】:

    您在初始化向量的子构造函数列表中。这不是真正的原子操作。因此在多线程系统中,您可能会同时受到两个线程的攻击。这正在改变 id 是什么。欢迎来到线程安全 101!

    将初始化移动到被锁包围的构造函数中,这样只有一个线程可以访问和设置向量。

    解决此问题的另一种方法是将其移动到单例模式中。但是每次获得对象时,您都要为锁定付费。

    现在你可以进入双重检查锁定之类的事情了:)

    http://en.wikipedia.org/wiki/Double-checked_locking

    【讨论】:

    • 双重检查锁定是一种明显的反模式;做对是非常困难的,而且从来没有必要。在他的例子中,如果他让sharedResource 成为单例,静态instance 函数可以获得锁,并返回一个std::shared_ptr,其“析构函数”对象释放它。 (这应该是多线程环境中可变单例的标准模式。)
    【解决方案3】:

    当一个对象被初始化时,它应该由一个线程拥有。然后当它完成初始化时,它就被共享了。

    如果有线程安全初始化这样的东西,那就意味着确保一个对象在被初始化之前不能被其他线程访问。

    当然,我们可以讨论原子变量的线程安全assignment。赋值不同于初始化。

    【讨论】:

    • 在他的示例中,正在初始化的对象(B 类型)只能从单个线程访问(我想)。他的问题是该对象的构造函数使用了全局资源。
    猜你喜欢
    • 2010-12-30
    • 1970-01-01
    • 2020-04-03
    • 2017-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多