第二章:线程同步精要


互斥量

互斥量保护临界区,任何时候最多只有一个线程在同一个互斥量保护的临界区内活动

使用互斥量的原则

  • 用RAII手法封装互斥量,即使用MutexLockGuard,不自己调用互斥量的lock()和unlock()
  • 只使用non-recursive互斥量:recursive mutex(递归的互斥量/可重入的互斥量)和non-recursive mutex(非递归的互斥量/不可重入的互斥量)的区别:同一个线程可以重复对一个recursive mutex加锁,但是如果重复对一个non-recursive mutex加锁会导致死锁。
  • 每次构造MutexLockGuard对象的时候,思考调用栈上已经持有的锁,防止加锁顺序不同导致死锁。
  • 死锁的调试方法:把线程调用栈打印出来分析;用PTHREAD_MUTEX_ERRORCHECK来排错。
  • 尽量不要用读写锁和信号量

RAII

MutexLock

class MutexLock : boost::noncopyable {
public:
       MutexLock():holder_(0) {
              pthread_mutex_init(&mutex_, NULL);
       }
       ~MutexLock() {
              assert(holder_ == 0);
              pthread_mutex_destroy(&mutex_);
       }
       void lock() {  //仅供MutexLockGuard调用
              pthread_mutex_lock(&mutex_);
              holder_ = CurrentThread::tid();
       }
       void unlock() {  //仅供MutexLockGuard调用
              holder_ = 0;
              pthread_mutex_unlock(&mutex_);
       }
       bool isLockedByThisThread() {
              return holder_ == CurrentThread::tid();
       }
       void assertLocked() {
              assert(isLockedByThisThread());
       }
       pthread_mutex_t* getPthreadMutex() { //仅供Condition调用
              return &mutex_;
       }
private:
       pthread_mutex_t mutex_;
       pid_t holder_;
};

MutexLockGuard

#include "MutexLock.h"
class MutexLockGuard {
public:
	explicit MutexLockGuard(MutexLock& mutex):mutex_(mutex){
		mutex_.lock();
	}
	~MutexLockGuard() {
		mutex_.unlock();
	}
private:
	MutexLock& mutex_;
};
#define MutexLockGuard(x) static_assert(false, "missing mutex guard var name"); 
//编译期断定,防止出现漏写变量名,生成临时变量马上被销毁,并没有上锁的情况

使用non-recursive mutex

一个使用recursive mutex存在问题的例子

MutexLock mutex;
vector<Foo> vec;

void post(const Foo& foo) {
       MutexLockGuard guard(mutex);
       vec.push_back(foo);
}
void traverse() {
       MutexLockGuard guard(mutex);
       for (auto itr = vec.begin(); itr != vec.end(); ++itr) {
              itr->doit(); //如果doit()调用了post(),可能导致vector扩容,进而迭代器失效,出错
       }
}

解决方法1
提供两种类型的post()函数,一种加锁,一种不加锁,并且用isLockedByThisThread()函数检查是否持有锁。

void post(const Foo& foo){
    MutexLockGuard guard(mutex);
       vec.push_back(foo);
}
void postWithLockHold(const Foo& foo){
    assert(mutex.isLockedByThisThread());
    vec.push_back(foo);
}

解决方法2
推迟修改:traverse()函数遍历数组的时候把需要修改的元素位置记下来,遍历完了再单独修改

解决方法3
copy-on-wtire:用shared_ptr模拟读写锁的功能

  • 对于write端,当引用计数为1时可以放心地写,当引用计数大于1时怎么处理?
  • 对于read端,读之前把引用计数加1,读完之后减1。
class Foo {
public:
	void doit();
};
typedef vector<Foo> FooList;
typedef shared_ptr<FooList> FooListPtr;
MutexLock mutex;
FooListPtr g_foos;

void traverse() {
	FooListPtr foos;
	{
		MutexLockGuard guard(mutex); //临界区只保护对g_foos的读
		foos = g_foos;
		assert(!g_foos.unique());
	}
	for (auto itr = foos->begin(); itr != foos->end(); ++itr) {
		itr->doit();
	}
}
void post(const Foo& f) {
	MutexLockGuard guard(mutex);  //加锁范围为整个复制和修改
	if (!g_foos.unique()) {
		g_foos.reset(new FooList(*g_foos));
	}
	assert(g_foos.unique());
	g_foos->push_back(f);
}

void Foo::doit() {
	Foo f;
	post(f);
}

int main() {
	g_foos.reset(new FooList);
	Foo f;
	post(f);
	traverse();
}

条件变量

条件变量学名为管程(monitor),用于线程被等待唤醒和唤醒一个或多个线程,通知条件变化或者资源可用。

  • 条件变量的正确用法:以线程安全的BlockingQueue和CountDownLatch为例

条件变量的封装Condition

#include "MutexLock.h"
class Condition {
public:
       Condition(MutexLock& mu):mutex_(mu){
              pthread_cond_init(&cond_, NULL);
       }
       ~Condition() {
              pthread_cond_destroy(&cond_);
       }
       void notify() {
              pthread_cond_signal(&cond_);
       }
       void notifyAll() {
              pthread_cond_broadcast(&cond_);
       }
       void wait() {
              pthrad_cond_wait(&cond_, mutex_.getPthreadMutex());
       }
private:
       MutexLock& mutex_;
       pthread_cond_t cond_;
};

BlockingQueue

#include <deque>
#include <cassert>

#include "MutexLockGuard.h"
#include "Condition.h"

class BlockingQueue {
public:
       int dequeue() {
              MutexLockGuard guard(mutex);
              while (queue.empty()) {
                     cond.wait(); //自动对mutex解锁并进入等待,当被唤醒时尝试对mutex加锁,如果对mutex加锁成功则判断队列是否为空,由于虚假唤醒现象,这里的while不能换成if
              }
              assert(!queue.empty()); //双重保障?
              int res = queue.front();
              queue.pop_front();
              return res;
       }
       void enqueue(int num) {
              MutexLockGuard guard(mutex);
              queue.push_back(num);
              cond.notify(); //也可以放到临界区之外
       }
private:
       MutexLock mutex;
       Condition cond;
       std::deque<int> queue;
};

CountDownLatch

CountDownLatch计时器:使用场合如主线程等待多个子线程完成初始化,多个子线程等待主线程的”起跑“命令

#include "MutexLockGuard.h"
#include "Condition.h"

class CountDownLatch {
public:
       explicit CountDownLatch(int count) :mutex_(), cond_(mutex_),count_(count) {}
       void wait() {
              MutexLockGuard guard(mutex_);
              while (count_ > 0)
                     cond_.wait();
       }
       void countDown() {
              MutexLockGuard guard(mutex_);
              count_--;
              if (count_ == 0)
                     cond_.notifyAll();
       }
private:
       MutexLock mutex_; //注意mutex_先于cond_的声明,因为mutex_需要先初始化
       Condition cond_;
       int count_;
};

线程安全的单例模式

用double checked locking实现单例模式也无法保障线程安全。
用pthread_once_t:int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));
功能:本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。

template <typename T>
class Singleton:boost::noncopyable {
public:
       static T& instance() {
              pthread_once(&ponce_, &Singleton::init);
              return *value_;
       }
private:
       Singleton();
       ~Singleton();
       static void init() {
              value_ = new T();
       }
       static pthread_once_t ponce_;
       static T* value_;
};

template <typename T> 
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;

template <typename T>
T* Singleton<T>::value_ = nullptr;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章