muduo学习笔记:chapter2&3 线程同步及多线程


chapter2 线程同步精要

1 互斥器

主要原则:

  1. 用RAII手法封装mutex的创建、销毁、加锁、解锁
  2. 只使用不可重入锁
  3. 不手工调用lock()和unlock()函数,交给栈上的Guard对象的构造和析构函数负责。Guard对象的生命期正好等于临界区,保证始终在同一个函数同一个scope里对某个mutex加锁和解锁(Scoped Locking)
  4. 构造Guard对象时,避免死锁

次要原则:

  1. 不使用跨进程的mutex,进程间通信只使用TCP sockets
  2. 加锁、解锁在同一个线程,线程a不能unlock线程b已经锁住的mutex(RAII自动保证)
  3. 别忘了解锁(RAII自动保证)
  4. 不重复解锁(RAII自动保证)
  5. 必要的时候使用PTHREAD_MUTEX_ERRORCHECK来排错

1.1 只使用不可重入锁(非递归锁)

可重入锁与不可重入锁的区别可参考博客

非递归的优越性:重复加锁会导致死锁,把程序的逻辑错误暴露出来

1.2 死锁

  • 有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。
  • 两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
  • 死锁的几种场景:
    • 忘记释放锁
    • 重复加锁
    • 多线程多锁,抢占锁资源

2 条件变量

互斥器是加锁原语,用来排他性地访问共享数据,它不是等待原语。

如果需要等待某个条件成立,应该使用条件变量。

条件变量的使用方式:

wait端:

  1. 与mutex一起使用,该布尔表达式的读写需受此mutex保护
  2. 在mutex上锁的时候调用wait()
  3. 把判断布尔条件和wait()放到while循环中

signal/broadcast端:

  1. 不一定要在mutex已上锁的情况下调用signal(理论上)
  2. 在signal之前一般要修改布尔表达式
  3. 修改布尔表达式要用mutex保护
  4. 注意区分signal和broadcast,broadcast表示状态变化、signal表示资源可用

3 不要使用读写锁和信号量

4 封装MutexLock、MutexLockGuard、Condition

class MutexLock : boost::nocopyable {
public:
    MutexLock() : holder_(0) {
        pthread_mutex_init(&mutex_, NULL);
    }
    
    ~MutexLock() {
        assert(holder_ == 0);
        pthread_mutex_destory(&mutex_);
    }
    
    bool isLockedByThisThread() {
        return holder_ == CurrentThread::tid();
    }
    
    void assertLocked() {
        assert(isLockedByThisThread());
    }
    
    void lock() {	//仅供MutexLockGuard调用
        pthread_mutex_lock(&mutex_);
        holder_ = CurrentThread::tid();
    }
    
    void unlock() {	//仅供MutexLockGuard调用
        holder_ = 0;
        pthread_mutex_unlock(&mutex_);
    }
    
    pthread_mutex_t* getPthreadMutex() {	//仅供Condition调用
        return &mutex_;
    }
private:
    pthread_mutex_t mutex_;
    pid_t holder_;
};
class MutexLockGuard : boost::nocopyable {
public:
    explicit MutexLockGuard(MutexLock& mutex) : mutex_(mutex) {
        mutex_.lock();
    }
    
    ~MutexLockGuard() {
        mutex_.unlock();
    }
    
private:
    MutexLock& mutex_;
};
#define MutexLockGuard(x) static_assert(false, "missing mutex guard var name")
class Condition : boost::nocopyable {
public:
    explicit Condition(MutexLock& mutex) : mutex_(mutex) {
        pthread_cond_init(&pcond_, NULL);
    }
    
    ~Condition() {
        pthread_cond_destory(&pcond_);
    }
    
    void wait() {
        pthread_cond_wait(&pcond_, mutex.getPthreadMutex());
    }
    void notify() {
        pthread_cond_signal(&pcond_);
    }
    void notifyAll() {
        pthread_cond_broadcast(&pcond_);
    }
    
private:
    MutexLock& mutex_;
    pthread_cond_t pcond_;
}

在这里插入图片描述

5 小结

  1. 线程同步的四项原则,尽量使用高层同步设施(线程池、队列、倒计时)
  2. 使用普通互斥器和条件变量完成剩余的同步任务,采用RAII惯用手法和Scoped Locking

6 借shared_ptr实现copy-on-write

用shared_ptr来管理共享数据:

  • shared_ptr是引用计数型智能指针,如果当前只有一个观察者,那么引用计数的值为148。
  • 对于write端,如果发现引用计数为1,这时可以安全地修改共享对象,不必担心有人正在读它。
  • 对于read端,在读之前把引用计数加1,读完之后减1,这样保证在读的期间其引用计数大于1,可以阻止并发写。
  • 对于write端,如果发现引用计数大于1,该如何处理?(拷贝一份,在副本上修改)

chapter3 多线程服务器的适用场合与常用编程模型

1 进程与线程

进程是程序运行的实例,每个进程都有自己独立的地址空间

可以类比为“人”,每个人都有自己的记忆,但不知道别人的记忆,通过谈话(消息传递)来交流。

线程的特点是共享地址空间,从而可以高效地共享数据

2 单线程服务器的常用编程模型

Reactor模式:non-blocking IO + IO multiplexing

程序的基本结构:一个事件循环(event loop),以事件驱动(event-driven)和事件回调的方式实现业务逻辑

在这里插入图片描述
优点:编程简单,效率不错。不仅可用于读写socket,连接的建立甚至DNS解析都可以用非阻塞方式进行,以提高并发度和吞吐量,对于IO密集型的应用是不错的选择

缺点:事件回调函数必须是非阻塞的,对于涉及网络IO的请求响应式协议,容易割裂业务逻辑,使其散布于多个回调函数中,相对不容易理解和维护

3 one loop per thread

程序里的每个IO线程有一个event loop,用于处理读写和定时事件。

优点:

  1. 线程数目固定,可以在程序启动时设置,不会频繁创建和销毁
  2. 可以方便地在线程间调配负载
  3. IO事件发生的线程是固定的,同一个TCP连接不必考虑事件并发

Eventloop代表线程的主循环,需要让哪个线程干活,就把timer或IO channel注册到哪个线程的loop里即可。

推荐模型:

one (event)loop per thread + thread pool

  • event loop(IO loop)用作IO multiplexing,配合NIO和定时器
  • thread pool用作计算,具体可是任务队列或生产者消费者队列

4 进程间通信只用TCP

4.1 TCP sockets与pipe

共同点:本质都是操作文件描述符来收发字节流

不同点:

sockets:可以跨主机,具有伸缩性,双向通信

pipe:进程间需要亲缘关系才能使用,进程间双向通信需要两个文件描述符

4.2 分布式系统中使用TCP长连接通信的好处

  1. 容易定位分布式系统中的服务之间的依赖关系。
  2. 通过接受和发送队列的长度来定位网络和程序故障

5 多线程服务器的适用场合

服务端程序的基本任务:处理并发连接

主要模型:

  1. 运行一个单线程的进程:不可伸缩,无法发挥多核机器的计算能力
  2. 运行一个多线程的进程:程序复杂
  3. 运行多个单线程的进程:
    1. 简单把模式1中的进程运行多份
    2. 主进程+worker进程
  4. 运行多个多线程的进程

5.1 必须用单线程的场合

  1. 程序可能会fork:只有单线程程序能fork
  2. 限制程序的CPU占用率:单线程程序能限制程序的CPU占用率

优点:简单

缺点:事件优先级不同,可能出现优先级反转,导致延迟

5.2 多线程的适用场景

提高响应速度,让IO和“计算”相互重叠,降低延迟。虽然多线程不能提高绝对性能,但能提高平均响应性能。

在这里插入图片描述

5.3 线程分类

  1. IO线程,这类线程的主循环是IO multiplexing,阻塞地等待在select/poll/epoll_wait系统调用上。这类线程也处理定时事件。
  2. 计算线程。主循环是blocking queue,阻塞地等待在condition variable上。这类线程一般位于线程池中,通常不涉及IO,因此要避免任何阻塞操作
  3. 第三方库使用的线程,比如logging,又比如database connection
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值