【问题标题】:Locking shared resources in constructor and destructor在构造函数和析构函数中锁定共享资源
【发布时间】:2012-07-25 15:02:52
【问题描述】:

我相信我至少掌握了 C++ 中多线程的基础知识,但是我从来没有能够在构造函数或析构函数中围绕共享资源锁定互斥锁得到明确的答案。我的印象是你应该锁定两个地方,但最近同事不同意。假装下面的类被多个线程访问:

class TestClass
{
public:

   TestClass(const float input) :
      mMutex(),
      mValueOne(1),
      mValueTwo("Text")
   {
      //**Does the mutex need to be locked here?
      mValueTwo.Set(input);
      mValueOne = mValueTwo.Get();
   }

   ~TestClass() 
   { 
     //Lock Here?
   }

   int GetValueOne() const
   {
      Lock(mMutex);
      return mValueOne;
   }

   void SetValueOne(const int value)
   {
      Lock(mMutex);
      mValueOne = value;
   }

   CustomType GetValueTwo() const
   {
      Lock(mMutex);
      return mValueOne;
   }

   void SetValueTwo(const CustomType type)
   {
      Lock(mMutex);
      mValueTwo = type;
   }

private:

   Mutex mMutex;
   int mValueOne;
   CustomType mValueTwo;
};

当然,通过初始化列表,一切都应该是安全的,但是构造函数中的语句呢?在析构函数中,做一个非作用域的锁,从不解锁(本质上只是调用 pthread_mutex_destroy)会有好处吗?

【问题讨论】:

  • 当您说“类”在多个线程之间使用时,我假设您的意思是 TestClass 类型的对象可能会在多个线程中使用。在这种情况下,您仍然只创建一个对象,因此您不需要在构造函数中锁定。如果两个线程同时在构造函数中,它们将生成 2 个单独的对象。锁定对象构造更有意义,以确保(例如)在对象完成构造之前不使用 mValueTwo。析构函数似乎应该锁定,以确保数据在被销毁时不被访问。
  • @Rollie 是的,我的意思是该对象将被共享。所以,如果我创建:
  • @Rollie:在类被破坏时访问类是实例生命周期管理的一个错误 - 发生这种情况时程序已经损坏(如果它可以在破坏时访问,它也可能发生之后)。
  • @Rollie 抱歉,按 Enter 太快了。是的,我的意思是该对象将被共享。所以,如果我创建:`TestClass* testPointer = new TestClass(1.1); ` 然后在构造函数完全返回之前不会为 testPointer 分配地址,因此没有其他线程可以访问该对象。这是一个正确的总结吗?
  • @GeorgFritzsche:很好,我唯一担心的是——如果确实发生了,期望的行为是什么?我自己不是 100% 确定 - 我会假设非作用域锁会永久阻止其他线程的访问,并且可能会使一个细微的错误更容易追踪。有没有更优雅的方式来处理此类错误?布雷特,如果 testPointer 本身在多个线程之间共享,则您需要防止两者一起进入该块。如果 2 个线程尝试像这样构造 testPointer,它们将处理它们认为共享的 2 个独立对象。

标签: c++ multithreading


【解决方案1】:

多个线程不能构造同一个对象,也不应允许任何线程在对象完全构造之前使用它。因此,在健全的代码中,没有锁定的构造是安全的。

破坏是一个稍微困难的情况。但同样,对对象进行适当的生命周期管理可以确保当某些线程仍有可能使用对象时,该对象永远不会被破坏。

共享指针可以帮助实现这一点,例如。 :

  • 在某个线程中构造对象
  • 将共享指针传递给需要访问对象的每个线程(包括在需要时构造它的线程)
  • 当所有线程释放共享指针时,对象将被销毁

但显然,还存在其他有效的方法。关键是在对象生命周期的三个主要阶段之间保持适当的界限:构造、使用和销毁。绝不允许这些阶段之间有重叠。

【讨论】:

  • 使用 shared_ptr 通常是不够的。这是因为最后一个删除 shared_ptr 的线程将运行析构函数,但该线程可能不是最后一个修改对象的线程。如果对象包含复杂的数据成员,例如向量或映射,并且析构函数没有获取和释放锁,则析构函数可以看到陈旧的内存并导致崩溃。
  • @MichiHenning :一个线程应该只在它完成对对象的所有操作之后释放它的共享指针——这是一个非常合理(如果不是很明显)的要求。因此,锁定是不必要的。过时的内存在理论上确实是可能的,但要解决这个问题,您需要一个内存屏障,而不是锁定。 (当然,一些流行的线程库包含具有某些锁定操作的屏障,所以也许这就是您的意思)
  • 这里是场景。在线程 A 中创建 shared_ptr 并将 shared_ptr(正确互锁)传递给线程 B。线程 A 更新 foo 的状态并丢弃其指针。稍后,线程 B 删除了它的 shared_ptr,这导致线程 B 调用 foo 的析构函数。如果自线程 A 上次更新 foo 以来线程 B 尚未越过内存屏障,则 foo 的析构函数将对陈旧数据进行操作。如果 foo 包含一个复杂的数据成员,例如一个地图,这可能会导致崩溃。你是对的,内存屏障是需要的。碰巧互斥体也会产生障碍。
  • 我必须收回这个。 shared_ptr 的减量是原子的,因此在调用线程没有首先越过障碍的情况下,不可能调用析构函数。我很抱歉。
【解决方案2】:

它们不必被锁定在构造函数中,因为此时任何外部人员都可以访问该数据的唯一方法是从构造函数本身传递它们(或执行一些未定义的行为,例如调用虚方法)。

[编辑:删除了关于析构函数的部分,因为正如评论正确断言,如果您尝试从可能已死的对象访问资源,您会遇到更大的问题]

【讨论】:

  • 如果另一个线程可能会在对象被销毁时访问它,那么您就有一个错误并且锁定将无济于事(考虑如果销毁线程首先获取互斥锁会发生什么)。另外,我看不出将互斥锁包装在 shared_ptr 中会达到什么效果。
  • 你是对的,我同意你有一个问题,如果你有任何机会从一个即将死亡的对象访问数据。 shared_ptr 提议是为了避免互斥体与对象一起被破坏的情况,但我想您必须在尝试访问该对象中的任何内容之前锁定该互斥体,所以如果它在对象内部,这将不起作用。我将编辑我的答案。
猜你喜欢
  • 1970-01-01
  • 2012-09-27
  • 1970-01-01
  • 1970-01-01
  • 2013-07-07
  • 1970-01-01
  • 2011-04-03
  • 2010-12-16
  • 2011-03-01
相关资源
最近更新 更多