【问题标题】:When is this lock-then-lock-again refactor a bad idea?什么时候再锁定再锁定重构是个坏主意?
【发布时间】:2014-10-08 19:23:44
【问题描述】:

我有两个同级类AB,我想重构它们,使AB 的父类,所以B 可以共享A 的代码。但是这种重构意味着,对于一个关键功能,将互斥锁锁定两次而不是一次。有什么理由不这样做?

A 类

class A{
     std::map<std::string, int> values;
     std::mutex mutex;
 public:
     //init and access functions elided

     void foo(std::string key, int v){
         auto i = values.find(key);
         if(i == values.end())return; //Actually report error
         {
             std::lock_guard<std::mutex> lock(mutex);
             i->second = v;
         }
     }
};

B 类

class B{
    std::map<std::string, int> values;
    std::map<std::string, std::vector<int> > history;
    std::mutex mutex;
public:
    //init and access functions elided

    void foo(std::string key, int v){
        auto i = values.find(key);
        if(i == values.end())return; //Actually report error
        auto i2 = history.find(key);
        if(i2 == history.end())return; //Actually report error
        {
            std::lock_guard<std::mutex> lock(mutex);
            i->second = v;
            i2->second.push_back(v);
        }
    }
 };

我想写的 B 类的替代方案是:

class C:public A{
    std::map<std::string, std::vector<int> > history;
public:
    //init and access functions elided

    void foo(std::string key, int v){
        A::foo(key,v);
        auto i2 = history.find(key);
        if(i2 == history.end())return; //Actually report error
        {
            std::lock_guard<std::mutex> lock(mutex);
            i2->second.push_back(v);
        }
    }
 };

访问函数也使用互斥锁来锁定它们的读取。出于这个问题的目的,假设 foo() 比这些类中的任何其他函数都被调用得更多。我们也可以假设所有对foo() 的调用都是序列化的;互斥锁用于其他使用访问函数的线程。

问。将一个互斥锁拆分为两个串行完成的锁不会增加任何新的死锁可能性?

问。与 A 类和 B 类的代码重复,或“隐藏”基类调用中的额外互斥锁是否会产生更大的代码异味?

问。与我在foo() 中执行的其他操作相比,锁定两次的额外开销是否微不足道? IE。我猜插入映射和向量所花费的时间至少是锁定互斥锁的 10 倍。

问。 class C 现在允许读取 values 与读取 history 不同步(即,如果另一个线程在 C::foo() 中间抓住了锁)。如果这被证明是一个问题,那么返回“复制 A 类和 B 类中的代码”是唯一的设计选择吗?

【问题讨论】:

  • IMO,您提出的最大问题是最后一个问题。如果A::fooi2-&gt;second.push_back(v) 之间的数据不一致,那么这是错误的,您将遇到竞争情况。并且根本没有办法使用A::foo 并保持单个锁来执行此操作(除非您使用递归互斥锁并锁定整个调用。)
  • if 之后的一行上多余的左大括号令人困惑……return 并不明显(你讨厌空格?)乍一看,它看起来像互斥锁如果条件为真,则采取
  • 用户可以直接拨打A::foo吗?如果是,那么您已经可以读取与history 不同步的values,因为它们可以独立更改。
  • 像这样的细粒度锁定通常不起作用。这完全取决于您对history 成员所做的else。仅仅在另一个成员函数中返回它的内容已经创建了一个陈旧的视图,它与你通过重构创建的竞赛没有什么不同。
  • @JonathanWakely 大括号是多余的吗?我放了大括号以显示锁的范围,因此在该块之后的任何代码中都不会持有锁。 (我看到我的代码已被编辑以缩进锁定范围;我没有这样做来区分它,现在它看起来确实让我更加困惑;但我不知道您的评论是在编辑之前还是之后!)

标签: c++ multithreading c++11


【解决方案1】:

这个替代方案怎么样,它添加了一个返回锁的foo_impl函数,所以它可以在C::foo中重复使用:

class A
{
  std::map<std::string, int> values;
  std::mutex mutex;
public:
  //init and access functions elided

  void foo(std::string key, int v)
  {
    foo_impl(key, v);
  }

protected:
  std::unique_lock<std::mutex> foo_impl(std::string key, int v)
  {
    auto i = values.find(key);
    if (i == values.end()) return {}; //Actually report error
    std::unique_lock<std::mutex> lock(mutex);
    i->second = v;
    return lock;
  }
};

class C : public A
{
  std::map<std::string, std::vector<int> > history;
public:
  //init and access functions elided

  void foo(std::string key, int v)
  {
    auto i2 = history.find(key);
    if (i2 == history.end()) return; //Actually report error
    if (auto lock = A::foo_impl(key,v))
      i2->second.push_back(v);
  }
};

这可确保对A::valuesC::history 的更新在一个锁下完成,因此A::values 无法在原始C::foo 的两个锁之间再次更新。

【讨论】:

    【解决方案2】:

    我不明白你为什么认为有必要锁定两次,这似乎是你想要做的?

    class A{
      std::map<std::string, int> values;
      std::mutex mutex;
    
    protected:
      void foo_unlocked(std::string key, int v){
        auto i = values.find(key);
        if(i != values.end())
          i->second = v;
      }
    };
    
    class C:public A{
      std::map<std::string, std::vector<int> > history;
    public:
      //init and access functions elided
    
      void foo(std::string key, int v){
        auto i2 = history.find(key);
        if(i2 == history.end())
          return; //Actually report error
    
        std::lock_guard<std::mutex> lock(mutex);
        i2->second.push_back(v);
        foo_unlocked(key, v);  // do the operation in A but unlocked...
     }
    

    };

    【讨论】:

    • 这在values.find(key) 持有锁时执行,而原件没有。也许这无关紧要,但我认为这是故意的,这样做是为了避免在潜在的缓慢查找期间保持外观。
    • 我认为重构它是一项微不足道的任务..?
    • 是的,看看我的回答:)
    • 是的,正如Jonathon所说:代码(例如查找)没有被锁定,遵循只锁定您需要的最短时间的原则。
    • 它也完全破坏了A::foo[_unlocked],因为它根本不再受到保护。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-04
    • 2021-11-29
    • 2011-10-23
    • 1970-01-01
    相关资源
    最近更新 更多