C++ 并发编程 —— 《Linux多线程服务端编程》笔记

好长一段时间没有写文章了,这次总结一下陈硕大大写的《Linux多线程服务端编程》一书第二章的读书笔记。

1.互斥器(mutex)

使用互斥器的基本原则:

  • 用RAII手法封装mutex的创建、销毁、加锁、解锁这四个操作。Java里面的synchronized语句和C#的using语句也有类似的效果,即保证所得生效期间等于一个作用于,不会因异常而忘记解锁。
  • 只用非递归的mutex(即不可重入的mutext)
  • 不手工调用lock()和unlock()函数,一切交给栈上的Guard对象的构造和析构函数负责。
  • 在每次构造Guard对象的时候,思考一路上(调用栈上)已经持有的锁,防止因为锁顺序不同而导致死锁。
  • 进程间通信使用socket,不要使用mutex。
  • 加锁、解锁在同一个线程(RAII会保证)。
  • 别忘记解锁(RAII会保证)。
  • 不要重复解锁(RAII会保证)。
  • 必要的时候可以考虑用PTHREAD_MUTEX_ERRORCHECK来排错。

基本的互斥器的代码如下:

class MutexLock {
public:
    MutexLock() {
        pthread_mutex_init(&_mutex, NULL);
    }

    ~MutexLock() {
        pthread_mutex_destroy(&_mutex);
    }

    MutexLock& operator=(MutexLock& other) = delete;
    MutexLock(MutexLock& other) = delete;

    void lock() {
        pthread_mutex_lock(&_mutex);
    }

    void unlock() {
        pthread_mutex_unlock(&_mutex);
    }

    pthread_mutex_t* getRawMutex() {
        return &_mutex;
    }

private:
    pthread_mutex_t _mutex;
};

class MutexGuard {
public:
    MutexGuard(MutexLock& mutex)
    :_lock(mutex) {
        _lock.lock();
    }

    ~MutexGuard() {
        _lock.unlock();
    }
private:
    MutexLock& _lock;
};

2.条件变量(condition variable)

如果需要等待某个条件成立,我们应该使用条件变量。条件变量顾名思义是一个或多个线程等待某个布尔表达式为真,即等待别的线程“唤醒”它。条件变量的学名叫管程(monitor)。Java Object内置的wait()、notify()、notifyAll()是条件变量。
对于wait端应该注意:

  • 必须与mutex一起使用,该布尔表达式的读写接收此mutex的保护
  • 在mutex已经上锁的时候才能调用wait()
  • 把判断布尔条件和wait()放到while循环中

如下是一个简单的Condition实现

class Condition {
public:
    Condition(MutexLock& mutex)
    :_mutex(mutex) {
        pthread_cond_init(&_cond, NULL);
    }

    ~Condition() {
        pthread_cond_destroy(&_cond);
    }

    void wait() {
        pthread_cond_wait(&_cond, _mutex.getRawMutex());
    }

    void notify() {
        pthread_cond_signal(&_cond);
    }

    void notifyAll() {
        pthread_cond_broadcast(&_cond);
    }
private:
    MutexLock& _mutex;
    pthread_cond_t _cond;
};

关于Spurious wakeup(虚假唤醒)

虚假唤醒在linux的多处理器系统中,在程序接收到信号前可能会发生。在Windows系统和JAVA虚拟机上也存在。在系统设计时应该可以避免虚假唤醒,但是这会影响条件变量的执行效率,而既然通过while循环就能避免虚假唤醒造成的错误,因此程序的逻辑就变成了while循环的情况。

3.copy-on-write

主要思想是这样的:让读的加锁粒度尽量的小,但当写数据的时候,新的读请求是阻塞的。

typedef std::shared_ptr<Map> MapPtr;    
MapPtr gData(new Map);     // 有一个数据是需要共享的

MapPtr getData() {   // 读数据只有这一段是加锁的
    MutexGuard guard(mutex);
    return gData;
}

// read threads
MapPtr _data = getData(); 
// process _data....

// write thread
MutexGuard guard(mutex);    // 写数据全场加锁
if(!gData.unique()) {
    MapPtr tmpData(new Map(gData.get()));
    gData.swap(tmpData);
}
// process _data ...

4.小结

记得两年前开始了解C++多线程编程的时候看了陈硕的这本书,工作两年,由于长时间使用Python,对C++已经有些陌生了,更别说C++多线程编程了,今天再次拾起这本书,重读前面两章就当复习了。
接下来我会抽周末看一下《C++ Concurrency In Action》,也会陆续写一些相关的读书笔记。