首页 > Note > 多线程编程中的一些原则

多线程编程中的一些原则

关于 C++ 多线程编程一的些基本知识可以参考本博客的《C++11/14 新特性(多线程)》 ,《Unix线程基础》。本章不是多线程编程教程,而是个人经验的一些总结。这些经验有一些可能是不正确的,希望在今后的编程中实践、改进。

线程同步的四项基本原则:

  1. 最低限度地共享对象。对象尽量不要暴露给别的线程,如果需要暴露,优先考虑 immutable对象。否则尽量使用同步措施来充分地保护它
  2. 尽量使用高级地并发编程构件,如 任务队列、生产者消费者模式等
  3. 只用非递归的互斥器和条件变量,慎用读写锁,尽量少用信号量
  4. 除了使用 atomic 整数外,不要自己编写 lock-free 代码,也不要用”内核级”同步原语

互斥器 Mutex

mutex 是最常用的同步原语,它保护一个临界区,任何时候最多只能有一个线程能够访问 mutex 保护的域。使用 mutex 主要是为了保护共享数据。一般原则有:

  • 使用 RAII手法封装 mutex 的创建、销毁、加锁、解锁操作,充分保证锁的有效期等于其作用域,而不会因为中途返回或异常而忘记解锁。这类似于 Java 的synchronized 或 C# 的 using 语句。
  • 使用非递归的 mutex
  • 尽量不要人为地调用 lock()unlock()函数,将这些操作交给栈上的 guard 对象,利用其构造与析构函数。
  • 不要跨线程地加解锁,避免在不同的函数中分别加锁\解锁,避免在不同的语句分支中加锁\解锁
  • 每当构造 guard 对象时,需要考虑栈上已有的锁,防止因加锁顺序不同而导致死锁
  • 避免跨进程的 mutex, 进程间通讯尽量使用 TCP sockets

只使用非递归地 mutex

mutex 可分为递归(recursive)非递归(non-recursive)两种,也叫做可重入(reentrant)非可重入(non-reentrant)。它们的区别就是,同一线程可以重复地对 recursive mutex 加锁,而不能重复对 non-recursive mutex 加锁。在同一个线程中多次对 non-recursive mutex 加锁会立刻导致死锁或程序崩溃。
它们的性能相近。其实 recursive mutex 会稍快一些(因为少了一个计数器),且它用进来更方便。也正是因为它方便,所以会隐藏一些问题。一个例子:

这个程序看起来是正确的。然而,当 do_sth() 函数中间接调用了 post()函数,程序将会死锁。如果将 mu 对象的类型改为std::recursive_mutex, 则 可能 导致迭代器失效,进而引发 crash。这个时候只需要打印出线程信息,就可以排查出错误。
要解决间接调用的问题,可以将 post() 分为两个版本,分别为 post()post_without_lock():

条件变量 condition_variable

mutex 是加锁原语,用来排它性地访问共享数据。condition_variable 是等待原语。其含义是一个或多个线程 p 阻塞地等待某个变量 c,一但 c 满足条件,线程 p 将被唤醒。条件变量的正确使用方式:

  1. 必须与 mutex 配合使用, mutex 用于保护一个布尔表达式 expr
  2. mutex 加锁后才可以调用 wait()
  3. expr 的判断和 wait 需要放到 while 循环中

这里不可以使用 if 来替代 while,因为可能出现 spurious_wakeup  :参考1,  参考2

 

===待续===

  1. 本文目前尚无任何评论.
  1. 本文目前尚无任何 trackbacks 和 pingbacks.