类和互斥体

时间:2014-06-14 23:28:08

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

假设我有一个代表一些名为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;
};

还假设编译器优化已经处理了虚拟调度。我想要一个意见,我应该使用inhertance或简单地将互斥锁置于原始类中。

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

编辑:注意,没有必须只有一个参数,问题是用一个n参数的类来抽象。

2 个答案:

答案 0 :(得分:3)

使用std::lock_guardlock_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;
  }
};

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

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

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

答案 1 :(得分:0)

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

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#会执行类似的操作。