- 对象销毁时出现的竞态条件:
- 析构对象时,其他线程是否正在执行该对象的成员函数;
- 在执行成员函数期间,对象不会被其他线程析构;
- 在调用成员函数之前,如何确定对象还活着。
- 线程安全的类:
- 多线程访问时,变现出正确的行为;
- 无论操作系统如何调度这些线程,以及线程的执行顺序;
- 调用端代码不需要额外的同步。
- 简单的线程安全类
class Counter
{
public:
Counter():value_(0){}
int value() const;
int getAndIncrease();
private:
int value_;
mutable MutexLock mutex_;
};
int Counter::value() const
{
MutexLockGuard lock(mutex_);
return value_;
}
int Counter::getAndIncrease()
{
MutexLockGuard lock(mutex_);
int ret=value_++;
return ret;
}
每个对象都有自己的mutex_,因此不同对象之间不构成锁争用。
- 存在的问题:销毁太难,析构函数调用的前提是成员变量mutex_被销毁。无法保证析构的线程安全。
- C++内存问题:
- 缓冲区溢出;
- 空悬指针/野指针
- 重复释放
- 内存泄漏
- 不配对的new/delete
- 内存碎片
线程同步精要
-
线程同步四项原则
- 尽量最低限度地共享对象,减少需要同步的场合。
- 使用高级并发编程构件。
- 最后不得已必须使用底层原语时,只用非递归的互斥器和条件变量,慎用读写锁,不要用信号量。
- 互斥器(mutex)使用原则:
- 用RAII手法封装mutex的创建、销毁、加锁、解锁。
- 不可重入mutex
- Lock()和unlock()函数的功能交给Guard对象的构造和析构。
- 不使用跨进程的mutex。
- 加解锁在同一个线程。
- 必要时可考虑用PTHREAD_MUTEX_ERRORCHECK来拍错。
- 锁小结
互斥锁保护了一个临界区,在这个临界区中,一次最多只能进入一个线程。如果有多个进程在同一个临界区内活动,就有可能产生竞态条件(race condition)导致错误。
读写锁从广义的逻辑上讲,也可以认为是一种共享版的互斥锁。如果对一个临界区大部分是读操作而只有少量的写操作,读写锁在一定程度上能够降低线程互斥产生的代价。
条件变量允许线程以一种无竞争的方式等待某个条件的发生。当该条件没有发生时,线程会一直处于休眠状态。当被其它线程通知条件已经发生时,线程才会被唤醒从而继续向下执行。
可递归锁与非递归锁:二者唯一的区别是,同一个线程可以多次获取同一个递归锁,不会产生死锁。而如果一个线程多次获取同一个非递归锁,则会产生死锁。
- MutexLock mutex;
- void foo()
- {
- mutex.lock();
- // do something
- mutex.unlock();
- }
- void bar()
- {
- mutex.lock();
- // do something
- foo();
- mutex.unlock();
- }
如果MutexLock锁是个非递归锁,则这个程序会立即死锁。但是这并不意味着应该用递归锁去代替非递归锁。递归锁用起来固然简单,但往往会隐藏某些代码问题。比如调用函数和被调用函数以为自己拿到了锁,都在修改同一个对象,这时就很容易出现问题。因此在能使用非递归锁的情况下,应该尽量使用非递归锁,因为死锁相对来说,更容易通过调试发现。程序设计如果有问题,应该暴露的越早越好。