互斥器(mutex)
互斥器主要是为了保护共享数据的,保证同一时刻只有一个线程可以操作
- 用RAII手法封装mutex(创建、销毁、加锁、解锁)
- 只用非递归的mutex(就是不可重入的mutex)
- 不手动调用lock和unlock函数,参照第一点的方式
- 在每次构造MutexLockGuard的时候,思考调用栈上已经持有的锁,防止加锁顺序不同导致死锁
- 不使用跨进程的mutex,进程间通信尽量只用TCP Sockets
- 加锁、解锁在同一个线程中完成,线程A不去unlock线程B已经锁住的mutex(RAII可以自动保证)
- 必要的时候可以用PTHREAD_MUTEX_ERRORCHECK来排查错误
条件变量(condition variable)
如果需要等待某个条件成立,我们应该使用条件变量。一般是一个或多个线程等待某个表达式为真,即等待别的线程唤醒自己
- 对于wait一端:
- 必须与mutex一起使用,该表达式的读写需要收到这个mutex的保护
- 在mutex加锁时才能调用wait
- 把判断表达式的条件和wait放到while循环中去
例子
MutexLock mutex;
Condition cond(mutex);
std::deque<int> deq;
int dequeue()
{
MutexLockGuard lock(mutex);
while (deq.empty()) //此处必须用循环,不能用if条件(if可能会虚假唤醒);必须在判断之后再wait()
{
cond.wait(); //这一步会unlock mutex 并进入等待,不会和enqueue死锁
//wait结束后,会自动加锁
}
assert(!deq.empty());
int top = deq.front();
deq.pop_front();
return top;
}
- 对于signal/broadcast一端:
- 在signal之前要修改表达式
- 修改表达式通常要用mutex保护
- 注意区分signal和broadcast:signal通常表示资源可用,broadcast通常表示状态变化
例子
void enqueue(int var)
{
MutexLockGuard lock(mutex);
queue.push_back(var);
cond.notify(); //换成notifyAll会有惊群现象,其中一个拿了锁并拿走了task,其余无事可做又睡了过去
}
总结
两点想法
1.应该先把程序写正确(并尽量保持清晰和简单),单后再考虑性能优化,如果确实还有必要优化的话。
2.让一个正确的程序变快,远比让一个快的程序便正确容易的多