linux 多线程服务端编程读书笔记(二)
第二章 线程同步精要
1、线程同步思想原则
- 尽量最低限度地共享对象,减少需要同步的场合。如果确实需要,优先考虑共享 immutable 对象。
- 使用高级的并发编程构建
- 不得已必须使用底层同步原语(primitives)时,只用非递归的互斥器和条件变量,慎用读写锁,不要用信号量。
- 除了使用 atomic 整数之外,不自己编写 lock-free 代码,也不要用“内核级”同步原语。不凭空猜测“哪种做法性能会更好”,比如 spin lock vs. mutex。
2、互斥器的使用原则
主要原则:
-
用 RAII 手法封装 mutex 的创建、销毁、加锁、解锁这四个操作。保证锁的生效期间等于一个作用域(scope)。
-
只用非递归的 mutex(即不可重入的 mutex)。使用非递归的锁调试更加的容易(栈的调用上分析)
注意:同一线程可以重复对递归锁加锁,但是不能对非递归锁进行重复加锁,递归锁需要有个计数机制
-
不手工调用 lock() 和 unlock() 函数,一切交给栈上的 Guard 对象的构造和析构函数负责(Scoped Locking)。
-
在每次构造 Guard 对象的时候,思考一路上(调用栈上)已经持有的锁,防止因加锁顺序不同而导致死锁。
次要原则:
- 不使用跨进程的mutex,进程间通信只使用TCP socket
- 加锁解锁在同一个线程,不能跨线程解锁
- 别忘了解锁(RAII自动保证)
- 不重复解锁(RAII自动保证)
- 必要的时候用PTHREAD_MUTEX_ERROCHECK来排错
自己与自己死锁的例子(多次加锁)
#include "../Mutex.h"
class Request
{
public:
void process() // __attribute__ ((noinline))
{
muduo::MutexLockGuard lock(mutex_);
print();//原本没有这一行,不小心被别人添加了
}
void print() const // __attribute__ ((noinline))
{
muduo::MutexLockGuard lock(mutex_);
}
private:
mutable muduo::MutexLock mutex_;
};
int main()
{
Request req;
req.process();
}
3、条件变量(学名为管程)
-
对于wait()端
- 必须与mutex一起使用,该布尔表达式的读写受mutex的保护
- 在mutex已经上锁的时候才能调用wait();
- 把判断布尔条件和wait()放到while循环中
mutable MutexLock mutex_; Condition notEmpty_(mutex_); boost::circular_buffer<T> queue_; T take() { MutexLockGuard lock(mutex_); while (queue_.empty()) { notEmpty_.wait(); } assert(!queue_.empty()); T front(queue_.front()); queue_.pop_front(); notFull_.notify(); // TODO: move outside of lock return front; }
-
对于signal/broadcast 端:
- 不一定要在mutex已经上锁的情况下调用signal(理论上)
- 在signal之前一般要修改布尔表达式
- 修改布尔表达式需要mutex保护
- broadcast 通常用于表明状态变化,signal 通常用于表示资源可用。
void put(const T& x) { MutexLockGuard lock(mutex_); queue_.push_back(x); notEmpty_.notify(); // TODO: move outside of lock }
上面就简单的实现了容量无限的BlockingQueue
4、不要用读写锁和信号量
- 正确性方面,一种典型的错误就是持有read lock时修改了共享数据。这通常发生在程序的维护阶段
- 性能方面:读写锁加锁的开销不一定比普通锁更搞笑
- 读锁可能允许被提升为写锁,也可能不能被提升,如果允许可能会造成迭代器失效,如果不允许可能会造成死锁,具体看书
- 通常读锁是可重入的,写锁是不可重入的
信号量
-
哲学家就餐问题的改进
一个线程专门管理餐具,其他线程那个号等在食堂门口
5、线程安全的Singleton实现
#ifndef MUDUO_BASE_SINGLETON_H
#define MUDUO_BASE_SINGLETON_H
#include <boost/noncopyable.hpp>
#include <pthread.h>
#include <stdlib.h> // atexit
namespace muduo
{
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();
::atexit(destroy);
}
static void destroy()
{
delete value_;
}
private:
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_ = NULL;
}
#endif
以上版本为通用版本,当然可以使用C++11
// 懒汉式单例模式
class Singleton
{
private:
Singleton() { }
static Singleton * pInstance;
public:
static Singleton * GetInstance()
{
if (pInstance == nullptr)
pInstance = new Singleton();
return pInstance;
}
};
// 线程安全的单例模式
class Singleton
{
private:
Singleton() { }
~Singleton() { }
Singleton(const Singleton &);
Singleton & operator = (const Singleton &);
public:
static Singleton & GetInstance()
{
static Singleton instance;
return instance;
}
};
6、sleep(3)不是同步原语
sleep()/usleep()/nanosleep() 只能出现在测试代码中。比如写单元测试的时候,或者用于有意延长临界区,加速复现死锁的情况。