避免使用多个互斥锁来保护类似的竞争条件

时间:2014-03-31 08:44:14

标签: c++ multithreading c++11 mutex race-condition

假设我有一些代码给我一个竞争条件,比如说

class foo
{
  some_data data;
public:
  void bar(some_type arg)
  {
    // may change data
  }
  // ...
};

在没有保护的情况下使用foo::bar()不会线程安全,因为另一个线程可能同时调用bar()。因此,可以说,更好的选择是

class foo
{
  some_data data;
  std::mutex my_mutex;
  void unsafe_bar(some_type arg);
public
  void bar(some_type arg)
  {
     std::lock_guard<std::mutex> lock(my_mutex);
     unsafe_bar(arg);
  }
};

但是,假设有许多类似的类,其成员函数具有同样的问题

class foo1 { public: void bar(some_type arg); /*...*/ };
class foo2 { public: void bar(some_type arg); /*...*/ };
class foo3 { public: void bar(some_type arg); /*...*/ };
class foo4 { public: void bar(some_type arg); /*...*/ };
class foo5 { public: void bar(some_type arg); /*...*/ };

每个在一个代码块内调用:

void work(some_type arg, foo1&f1, foo2&f2, foo3&f3, foo4&f4, foo5&f5)
{
  f1.bar(arg);
  f2.bar(arg);
  f3.bar(arg);
  f4.bar(arg);
  f5.bar(arg);
}

现在,这些调用中的每一个都锁定并解锁另一个互斥锁,这似乎效率低下。最好只使用一个互斥锁。

有推荐/最好的方法吗?

我在考虑以下设计:

class foo
{
  std::mutex my_mutex;
  void unsafe_bar(some_type arg);
public:
  template<typename Mutex>
  void bar(some_type arg, Mutex&m)
  {
    std::lock_guard<Mutex> lock(m);
    unsafe_bar(arg);
  }
  void bar(some_type arg)
  {
    bar(arg,my_mutex);
  }
};

现在,用户可以依赖foo::my_mutexfoo::bar的第二版),或者 提供互斥锁,可以是std::recursive_mutexnull_mutex(满足互斥锁概念,但不执行任何操作)。 这是一个明智/有用的想法吗?

编辑我注意到我的种族实际上并不依赖于任何特定的迭代器(以及它可能的失效等)。我删除了对迭代器的任何引用。

2 个答案:

答案 0 :(得分:2)

tl; dr 最终,您在对象上设置线程同步的方式取决于您,并且从单独的对象调用互斥锁上的lock可能效率不高,您可以考虑到你的整体设计并弄清楚&#39;哪里有&#39;您希望您的互斥锁/信号量和其他同步对象能够最好地处理与使代码“线程安全”相关的开销。如果您正在编写将在外部使用的代码(如库代码),那么您需要确保记录某个特定功能实际上是线程安全的&#39;否则我(作为用户)会自己保护我自己的代码。

void work(some_iterator const&i, foo1&f1, foo2&f2, foo3&f3, foo4&f4, foo5&f5)
{
    f1.bar(i);
    f2.bar(i);
    f3.bar(i);
    f4.bar(i);
    f5.bar(i);
}

问有没有推荐/最好的方法呢?

是的,从foo对象中取出互斥锁并将其放在其他地方;

// defined somewhere
std::mutex _mtx;

void work(some_iterator const&i, foo1&f1, foo2&f2, foo3&f3, foo4&f4, foo5&f5)
{
    _mtx.lock();
    f1.bar(i);
    f2.bar(i);
    f3.bar(i);
    f4.bar(i);
    f5.bar(i);
    _mtx.unlock();
}

template<typename Mutex> void bar(some_iterator const&i, Mutex&m)是一个明智/有用的想法吗?

不,正如上面的例子所示,呼叫全球&#39;更简单(也更清洁)。互斥/ semphore对象;如果我想使用你的模板化功能做同样的事情,我会有额外的打字,额外的打电话,很可能不是我想要的效果,例如:

// defined somewhere
std::mutex _mtx;

void work(some_iterator const&i, foo1&f1, foo2&f2, foo3&f3, foo4&f4, foo5&f5)
{
    f1<std::mutex>.bar(i, _mtx);
    f2<std::mutex>.bar(i, _mtx);
    f3<std::mutex>.bar(i, _mtx);
    f4<std::mutex>.bar(i, _mtx);
    f5<std::mutex>.bar(i, _mtx);
}

我必须特别了解work函数中的互斥锁,因为我的some_iterator可能会在工作函数之外被修改(即使每个foo void work(some_iterator const&i, foo1&f1, foo2&f2, foo3&f3, foo4&f4, foo5&f5) { // i could be modified here f1.bar(i); // here if mutex handle != others // here f2.bar(i); // here if mutex handle != others // here f3.bar(i); // here if mutex handle != others // here f4.bar(i); // here if mutex handle != others // here f5.bar(i); // here if mutex handle != others // here } 1}}对象有一个互斥锁),例如:

work

当然,这可能是你想要的?如果work函数本质上不是原子的,那么我(作为函数的用户)可能会在std::mutex _mtx2; void some_thread_fn1() { _mtx2.lock(); // some other code work(i, f1, f2, f3, f4, f5); _mtx2.unlock(); } void some_thread_fn2() { _mtx2.lock(); // some other code work(i, f1, f2, f3, f4, f5); _mtx2.unlock(); } 函数周围放置一个互斥锁,例如(假设上面的代码): / p>

work

谁曾使用foo函数需要(有些)意识到在其中调用互斥锁,否则它们可能会加倍保护(击败你的原始目的)。

另请注意,您的所有代码都不会实际锁定相同的互斥锁(假设您的std::mutex类中也包含class foo1 { some_data data; std::mutex my_mutex; void unsafe_bar(some_iterator const&i); public: void bar(some_iterator const&i) { std::lock_guard<std::mutex> lock(my_mutex); unsafe_bar(i); } }; class foo2 { some_data data; std::mutex my_mutex; void unsafe_bar(some_iterator const&i); public: void bar(some_iterator const&i) { std::lock_guard<std::mutex> lock(my_mutex); unsafe_bar(i); } }; foo1 f1; foo2 f2; some_iterator x; void thread1() { f1.bar(x); } void thread2() { f2.bar(x); } 个),例如:

{{1}}

上面的代码会导致竞争条件,因为您要锁定2个单独的互斥锁(因为您想要锁定同一个互斥锁,所以会出现逻辑错误)。

这个问题似乎更多地是关于多线程设计和最佳实践,正如另一个答案所指出的那样。

希望可以提供帮助。

答案 1 :(得分:1)

  

有推荐/最好的方法吗?

我认为你的方法很常见,是避免竞争条件的好方法。

  

这是一个明智/有用的想法吗?

我认为第二种方法不仅值得怀疑。您将把同步的负担放在您班级的客户/用户身上。至少每当使用模板化函数时。但是如果你这样做,你可以声明你的类不是线程安全的,并且调用者必须在所有情况下同步它。哪个至少是一致的。

通常,您应该同步数据,同时访问的数据成员。您的互斥锁的数量更多地是关于粒度。例如,总是锁定一个完整的块或仅锁定读/写操作。但这取决于用例。

修改: 因为你现在发布了一些代码 这仍然取决于你想如何使用你的课程 f1到f5是否仅用于 void work(some_iterator const&i, foo1&f1, foo2&f2, foo3&f3, foo4&f4, foo5&f5) 方法。然后根本不要使用互斥锁。类的用户很容易进行同步。 work中的互斥锁。
这些类是否与工作方法分开使用?
同样,答案将是保护与其自己的互斥锁一起使用的每个类成员,因为对于客户端来说,它将更加困难。