linux 多线程服务端编程读书笔记(二)

linux 多线程服务端编程读书笔记(二)

第二章 线程同步精要

1、线程同步思想原则
  1. 尽量最低限度地共享对象,减少需要同步的场合。如果确实需要,优先考虑共享 immutable 对象。
  2. 使用高级的并发编程构建
  3. 不得已必须使用底层同步原语(primitives)时,只用非递归的互斥器和条件变量,慎用读写锁,不要用信号量。
  4. 除了使用 atomic 整数之外,不自己编写 lock-free 代码,也不要用“内核级”同步原语。不凭空猜测“哪种做法性能会更好”,比如 spin lock vs. mutex。
2、互斥器的使用原则

主要原则:

  1. 用 RAII 手法封装 mutex 的创建、销毁、加锁、解锁这四个操作。保证锁的生效期间等于一个作用域(scope)。

  2. 只用非递归的 mutex(即不可重入的 mutex)。使用非递归的锁调试更加的容易(栈的调用上分析)

    注意:同一线程可以重复对递归锁加锁,但是不能对非递归锁进行重复加锁,递归锁需要有个计数机制

  3. 不手工调用 lock() 和 unlock() 函数,一切交给栈上的 Guard 对象的构造和析构函数负责(Scoped Locking)。

  4. 在每次构造 Guard 对象的时候,思考一路上(调用栈上)已经持有的锁,防止因加锁顺序不同而导致死锁。

次要原则:

  1. 不使用跨进程的mutex,进程间通信只使用TCP socket
  2. 加锁解锁在同一个线程,不能跨线程解锁
  3. 别忘了解锁(RAII自动保证)
  4. 不重复解锁(RAII自动保证)
  5. 必要的时候用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、条件变量(学名为管程)
  1. 对于wait()端

    1. 必须与mutex一起使用,该布尔表达式的读写受mutex的保护
    2. 在mutex已经上锁的时候才能调用wait();
    3. 把判断布尔条件和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;
      }
    
  2. 对于signal/broadcast 端:

    1. 不一定要在mutex已经上锁的情况下调用signal(理论上)
    2. 在signal之前一般要修改布尔表达式
    3. 修改布尔表达式需要mutex保护
    4. broadcast 通常用于表明状态变化,signal 通常用于表示资源可用。
    void put(const T& x)
      {
        MutexLockGuard lock(mutex_);
        queue_.push_back(x);
        notEmpty_.notify(); // TODO: move outside of lock
      }
    
    

    上面就简单的实现了容量无限的BlockingQueue

4、不要用读写锁和信号量
  1. 正确性方面,一种典型的错误就是持有read lock时修改了共享数据。这通常发生在程序的维护阶段
  2. 性能方面:读写锁加锁的开销不一定比普通锁更搞笑
  3. 读锁可能允许被提升为写锁,也可能不能被提升,如果允许可能会造成迭代器失效,如果不允许可能会造成死锁,具体看书
  4. 通常读锁是可重入的,写锁是不可重入的

信号量

  1. 哲学家就餐问题的改进

    一个线程专门管理餐具,其他线程那个号等在食堂门口

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() 只能出现在测试代码中。比如写单元测试的时候,或者用于有意延长临界区,加速复现死锁的情况。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值