类和互斥锁

5
假设我有一个代表某个数据结构的类,名为 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以用于并发目的,这样fooSingleThreadUserClass就可以继续使用没有互斥锁的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个参数的类。

5
首先,您应该使用“std::lock_guard”来锁定您的互斥量,可能还要使您的互斥量“mutable”。 - Stephan Dollberg
3
如果只是一个 int,您可以使用 std::atomic_int - chris
一个无争议的互斥锁通常是非常便宜的。我会将其添加到类中,只有在遇到性能问题(并且确信是互斥锁减慢了速度)时才进行优化。 - zmb
3
真正的类不仅拥有一个数据成员。确保对象始终处于一致状态,所有数据成员都得到同步,这不能在类本身中实现,而必须由客户端代码完成。将此任务交给最有可能做错的程序员。线程编程很难,我们去购物吧。 - Hans Passant
@HansPassant,我不同意。在对象级别上进行操作是最简单的方式。然而,所有操作数据的函数都应该有一个std::lock_guard来确保它们保持同步。有时候你需要同步多个对象,在这种情况下,把同步实现在对象外部会变得更加复杂(至少在我的实践中,同步一组对象比较困难,除非它们本身是另一个对象的一部分,而该对象又可以按照每个公共函数进行同步)。 - Alexis Wilke
2个回答

4
使用 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;
  }
};

如果您需要能够从const函数锁定或解锁mutex,则可以在mutex上放置mutable。通常我会在特别需要时才将mutablemutex中去掉。
性能是否会下降?这取决于情况。如果您调用该函数一百万次,那么创建mutex的开销可能会成为问题(它们不便宜)。如果函数执行时间很长,并且被许多线程频繁调用,则快速阻塞可能会影响性能。如果您无法确定具体问题,请使用std::lock_guard
Hans Passant提出了一个超出您问题范围的有效关注点。我认为Herb Sutter(?)在他的网站文章中写过这个问题。不幸的是,我现在找不到它。要理解为什么多线程编程如此困难,以及为什么单个数据字段上的锁定“不足够”,请阅读一本多线程编程的书籍,例如C++ Concurrency in Action: Practical Multithreading

2
请返回翻译后的文本 - T.C.
谢谢,我以前不知道 lock_guard 的存在。这比手动操作更安全。 - JoeyAndres

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接