多线程同步

来源:互联网 发布:木门下单软件 编辑:程序博客网 时间:2024/06/03 23:40

读陈硕先生的《moduo多线程服务器编程》第二章线程同步精要,做下笔记。

编程概要

  1. 首要原则是尽量最低限度共享对象,减少需要同步的场合。
  2. 其次是使用高级的并发编程组件,如TaskQueue、Producer-Consumer Queue、CountDownLatch等. [链接](https://github.com/chenshuo/muduo)
  3. 最后不得已必须使用底层同步原语时,只用非递归的互斥器和条件变量,慎用读写锁,不要用信号量。
  4. 除了使用atomic整数之外,不自己写lock-free代码,也不要用“内核级”同步原语。

互斥器(mutex)

互斥器用于保护临界区,任何一个时刻最多只能有一个线程在此mutex划出的临界区内活动,单独使用mutex时,主要是为了保护共享资源。编程原则:
1. 用RAII手法封装mutex创建,销毁,加锁,解锁这四个操作。具体代码见最后。
2. 只用非递归的mutex(不可重入的mutex)。因为少用一个计数器,比递归的mutex略快一点,但主要还是为了设计意图,不是为了性能考虑。在同一个线程中多次对非递归mutex加入会立刻死锁,能帮助我们及早发现问题。
3. 不手工调用lock()和unlock()函数,一切交给栈上的Guard对象构造函数和析构函数负责。
4. 每次构造Guard对象,思考一路上(调用栈上)已经持有的锁,防止因加锁顺序不同而导致死锁。

次要原则:
1. 不使用跨进程的mutex,进程间通信只用TCP sockets。
2. 加锁、解锁都在同一个线程,线程a不能去unlock线程b已经锁住的mutex。
3. RAII保证解锁与不重复解锁。
4. 必要的时候可以考虑PTHREAD_MUTEX_ERRORCHECK来排错。

note: Linux的Pthreads mutex 采用 futex 实现,不必每次加锁、解锁都陷入内核。

条件变量(cond)

如果需要等待某个条件成立,应该使用条件变量,学名管程。
对于wait端:
1. 必须和mutex一起使用,该布尔表达式的读写需受此mutex保护。
2. 在mutex已上锁的时候才能调用wait()。
3. 把判断布尔条件和wait()放到while循环中。

例子1:
链接

例子2:

muduo::MutexLock mutex;muduo::Condition cond(mutex);std::deque<int> queue;int dequeue(){    MutexLockGuard lock(mutex);    while(queue.empty()){        cond.wait();    }    int top = queue.front();    queue.pop_front();    return pop;}

note:必须使用while循环来等待条件变量,不是使用if语句,原因是spurious wakeup。

对于signal/broadcast端:
1. 不一定要在mutex已上锁的情况下调用signal
2. 在signal之前一般要修改布尔表达式。
3. 修改布尔表达式通常要用mutex保护。
4. broadcast通常用于表明状态变化,signal通常用于表示资源可用。

例子1:
链接

例子2:

void enqueue(int x){    MutexLockGuard lock(mutex);    queue.push_back(x);    cond.notify();}

note:互斥器和条件变量构成了多线程编程的全部必备同步源于,用它们即可完成任何多线程同步任务,二者不可相互替代。

不用读写锁和信号量

  1. 正确性上:易发生在持有read lock时候修改了共享数据,这种错误的后果跟无保护并发读写共享数据时一样的。
  2. 性能上:读写锁不见得比普通mutex更高效,如果临界区很小,锁竞争不激烈,mutex往往更快。
  3. 通常reader lock是可重入的,writer lock是不可重入的。但为了防止writer饥饿,writer通常会阻塞后来的reader lock,因此reader lock在重入的时候可能死锁。

不用信号量:
1. 使用条件变量配合互斥器可以完全替代信号量,而且不易出错。
2. 信号量增加了程序设计的负担和出错的可能。

代码

MutexLock:
链接

MutextLockGuard:
链接

Condition:
链接

原创粉丝点击