第2章 线程同步精要

发布时间 2023-04-01 21:29:32作者: 好人~

第2章 线程同步精要

线程同步的四项原则,按重要性排列:

  • 1.首要原则是尽量最低限度地共享对象,减少需要同步的场合。一个对象能不暴露给别的线程就不要暴露;如果要暴露,优先设置对象不可更改;实在不行才暴露可修改的对象,并用同步措施来充分保护它。
  • 2.其次是使用高级的并发编程构件,如TaskQueue、Producer- Consumer Queue、CountDownLatch等等。
  • 3.最后不得已必须使用底层同步原语(primitives)时,只用非递归的互斥器和条件变量,慎用读写锁,不要用信号量。
  • 4.除了使用atomic整数之外,不自己编写lock-free代码3,也不要用“内核级”同步原语4 5。不凭空猜测“哪种做法性能会更好”,比如spin lock vs. mutex。

2.1 互斥器(mutex)

单独使用mutex时,我们主要为了保护共享数据。我个人的原则是:

  • 使用unique_lock管理mutex,使用构造和析构函数来进行加锁和解锁,而不是自己收到加锁和解锁。
  • 只用非递归的mutex(即不可重入的mutex)。
  • unique_lock对象一般设置为栈上对象,然后看函数调用栈就能分析用锁的情况,非常便利。【不懂】

次要原则有:

  • 不使用跨进程的mutex,进程间通信只用TCP sockets。
  • 加锁、解锁在同一个线程,线程a不能去unlock线程b已经锁住的mutex(unique_lock自动保证)。
  • 必要的时候可以考虑用PTHREAD_MUTEX_ERRORCHECK来排错。【不懂】

2.1.1 只使用非递归的mutex

在同一个线程里多次对non-recursive mutex加锁会立刻导致死锁,我认为这是它的优点,能帮助我们思考代码对锁的期求,并且及早(在编码阶段)发现问题。
recursive mutex可能会隐藏代码里的一些问题。典 型情况是你以为拿到一个锁就能修改对象了,没想到外层代码已经拿 到了锁,正在修改(或读取)同一个对象呢。如下:

#include "../Mutex.h"
#include "../Thread.h"
#include <vector>
#include <stdio.h>

using namespace muduo;

class Foo
{
 public:
  void doit() const;
};

MutexLock mutex;
std::vector<Foo> foos;

void post(const Foo& f)
{
  MutexLockGuard lock(mutex);
  foos.push_back(f);
}

void traverse()
{
  MutexLockGuard lock(mutex);
  for (std::vector<Foo>::const_iterator it = foos.begin();
      it != foos.end(); ++it)
  {
    it->doit();
  }
}

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

int main()
{
  Foo f;
  post(f);
  traverse();
}