【问题标题】:Classes and Mutex类和互斥体
【发布时间】:2014-06-14 23:28:08
【问题描述】:

假设我有一个类来表示一些名为 foo 的数据结构:

class foo{
  public:
    foo(){
      attr01 = 0;
    }
    void f(){
      attr01 += 5;
    }
  private:
    int attr01;
};

class fooSingleThreadUserClass{
    void usefoo(){
      fooAttr.f();
    }

    foo fooAttr;
}

现在应该在软件构建的后期,我发现我需要多线程。我应该在 foo 中添加互斥锁吗?

class foo{
  public:
    foo(){
      attr01 = 0;
    }
    void f(){
      attr01Mutex.lock();
      attr01 += 5;
      attr01Mutex.unlock();
    }
  private:
    int attr01;
    std::mutex attr01Mutex;
};

class fooMultiThreadUserClass{
    void usefoo(){
      std::thread t1(&fooMultiThreadUserClass::useFooWorker, this);
      std::thread t2(&fooMultiThreadUserClass::useFooWorker, this);
      std::thread t3(&fooMultiThreadUserClass::useFooWorker, this);
      std::thread t4(&fooMultiThreadUserClass::useFooWorker, this);

      t1.join();
      t2.join();
      t3.join();
      t4.join();
    }

    void useFooWorker(){
      fooAttr.f();
    }

    foo fooAttr;
}

我知道 fooMultiThreadUserClass 现在可以在没有竞争的情况下以高性能运行 foo,但是 fooSingleThreadUserClass 会因为互斥量开销而降低性能吗?我很想知道。或者我应该出于并发目的从 foo 派生 fooCC,以便 fooSingleThreadUserClas 可以继续使用没有互斥锁的 foo,而 fooMultiThreadUserClass 使用带有互斥锁的 fooCC,如下所示

class fooCC : public foo{
  public:
    foo(){
      attr01 = 0;
    }
    void f(){  // I assume that foo::f() is now a virtual function.
      attr01Mutex.lock();
      foo::f();
      attr01Mutex.unlock();
    }
  private:
    std::mutex attr01Mutex;
};

还假设编译器优化已经处理了虚拟调度。我想知道我应该使用继承还是简单地将互斥锁放在原始类中。

我已经搜索过 Stackoverflow,但我想我的问题有点太具体了。

编辑:请注意,不一定只有一个参数,问题是抽象的,带有 n 个参数的类。

【问题讨论】:

  • 首先你应该使用std::lock_guard 来锁定你的互斥锁,并且可能还让你的互斥锁mutable。
  • 如果只有一个int,可以使用std::atomic_int。
  • 无竞争的互斥锁通常很便宜。我会将它直接添加到类中,并且仅在遇到性能问题时进行优化(并且确信它是互斥体减慢了速度)。
  • 真正的类不只有一个数据成员。确保对象始终处于一致状态,同步所有数据成员,永远不能在类本身中实现。它必须由客户端代码完成。把工作交给最不可能做对的程序员。线程很难,我们去购物吧。
  • @HansPassant,我不同意。在对象级别制作是最简单的方法。但是,所有操作数据的函数都应该有一个std::lock_guard 以确保它保持同步。碰巧您需要同步多个对象,在这种情况下,但必须在对象之外实现它会使它变得更加复杂(至少在我的实践中,同步一堆对象要困难得多,除非它们本身是另一个对象的一部分,该对象本身可以在每个公共函数的基础上同步。)

标签: c++ multithreading c++11 mutex


【解决方案1】:

使用std::lock_guard。 lock_guard 在其构造函数中采用 mutex。在构建过程中,lock_guard 锁定了mutex。当lock_guard 超出范围时,其析构函数会自动释放锁。

class foo
{
private:
  std::mutex mutex;
  int attr01;

public:
  foo() {
    attr01 = 0;
  }

  void f(){
    std::lock_guard<std::mutex> lock (mutex);
    attr01 += 5;
  }
};

如果您需要能够从const 功能锁定或解锁mutex,您可以将mutable 放在mutex 上。我通常将mutable 与mutex 分开,直到我特别需要它。

它会失去性能吗?这取决于。如果您调用该函数一百万次,那么创建mutex 的开销可能会成为一个问题(它们并不便宜)。如果函数执行时间长,并且被很多线程频繁调用,那么快速阻塞可能会影响性能。如果您无法确定具体问题,请使用std::lock_guard。

Hans Passant 提出了一个超出您问题范围的有效问题。我认为 Herb Sutter (?) 在他的一篇网站文章中写到了这一点。不幸的是我现在找不到它。要了解为什么多线程如此困难,以及为什么锁定单个数据字段是“不够的”,请阅读一本关于多线程编程的书,例如 C++ Concurrency in Action: Practical Multithreading

【讨论】:

【解决方案2】:

每个对象互斥有时是个好主意,但这种方法不是模块化的。考虑这个例子:

using namespace std;

struct LimitCounter {
    int balance = 1000;
    mutex lock;
    bool done() const { 
        lock_guard<mutex> g(lock);
        return balance == 0; 
    }

    void dec() {
        lock_guard<mutex> g(lock);
        balance--;
    }
};

以及此限制计数器的用户:

LimitCounter counter;  // global context

// JobRunner run some job no more than 1000 times
struct JobRunner {
    motex lock;
    void do_the_job() {
        lock_guard<mutex> g(lock);
        if (!counter.done()) {
            ...actually do the job...
        }
        counter.dec();
    }
};

此代码是线程安全的,但不正确(在多线程环境中平衡可能变为负数,作业将被执行超过 1000 次)。两个正确同步的对象的组合不会给我们正确的结果。

要使其正确,您必须在 JobRunner 类的所有实例之间共享一个锁。它必须在 counter.done() 检查之前锁定并在 counter.dec() 之后解锁。换句话说 - 锁层次结构必须与对象层次结构分离。 在哪里放置这个锁是个人喜好的问题。您可以在 JobRunner::do_the_job 中锁定 LimitCounter::lock,或者您可以将 JobRunner::lock 设为静态变量,您可以将互斥锁作为参数传递给 JobRunner::do_the_job。

另一种情况是当您拥有大量对象时。在这种情况下,您不能只为每个对象添加一个互斥锁,因为它太昂贵(每个互斥锁都是一个内核对象,您可能会用完句柄)。在这种情况下,您可以对对象进行分片并使用相同的互斥锁锁定每个分片。例如:

mutex mutexes[0x1000]; 
....
struct UbiquitousResource {
    int unique_id;
    void do_some_job() {
        auto& m = mutexes[hash(unique_id) & 0xFFF];
        lock_guard<mutex> g(m);
        ...do the job...
    }
};

我相信当您使用同步方法(在 Java 中)或锁定对象(在 C# 中)时,Java 和 C# 会做类似的事情。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-11
    • 2013-06-08
    • 2018-05-23
    • 2013-10-08
    • 2013-07-07
    相关资源
    最近更新 更多