假设我有一个代表一些名为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参数的类来抽象。
答案 0 :(得分:3)
使用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;
}
};
如果您需要能够从mutable
功能锁定或解锁mutex
,则可以将mutex
放在const
上。我通常会将mutable
从mutex
移开,直到我特别需要它为止。
会失去性能吗?这取决于。如果你正在调用该函数一百万次,那么创建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#会执行类似的操作。