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
    评论
ava实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),可运行高分资源 Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。下面详细介绍C语言的基本概念和语法。 1. 变量和数据类型 在C语言中,变量用于存储数据,数据类型用于定义变量的类型和范围。C语言支持多种数据类型,包括基本数据类型(如int、float、char等)和复合数据类型(如结构体、联合等)。 2. 运算符 C语言中常用的运算符包括算术运算符(如+、、、/等)、关系运算符(如==、!=、、=、<、<=等)、逻辑运算符(如&&、||、!等)。此外,还有位运算符(如&、|、^等)和指针运算符(如、等)。 3. 控制结构 C语言中常用的控制结构包括if语句、循环语句(如for、while等)和switch语句。通过这些控制结构,可以实现程序的分支、循环和多路选择等功能。 4. 函数 函数是C语言中用于封装代码的单元,可以实现代码的复用和模块化。C语言中定义函数使用关键字“void”或返回值类型(如int、float等),并通过“{”和“}”括起来的代码块来实现函数的功能。 5. 指针 指针是C语言中用于存储变量地址的变量。通过指针,可以实现对内存的间接访问和修改。C语言中定义指针使用星号()符号,指向数组、字符串和结构体等数据结构时,还需要注意数组名和字符串常量的特殊性质。 6. 数组和字符串 数组是C语言中用于存储同类型数据的结构,可以通过索引访问和修改数组中的元素。字符串是C语言中用于存储文本数据的特殊类型,通常以字符串常量的形式出现,用双引号("...")括起来,末尾自动添加'\0'字符。 7. 结构体和联合 结构体和联合是C语言中用于存储不同类型数据的复合数据类型。结构体由多个成员组成,每个成员可以是不同的数据类型;联合由多个变量组成,它们共用同一块内存空间。通过结构体和联合,可以实现数据的封装和抽象。 8. 文件操作 C语言中通过文件操作函数(如fopen、fclose、fread、fwrite等)实现对文件的读写操作。文件操作函数通常返回文件指针,用于表示打开的文件。通过文件指针,可以进行文件的定位、读写等操作。 总之,C语言是一种功能强大、灵活高效的编程语言,广泛应用于各种领域。掌握C语言的基本语法和数据结构,可以为编程学习和实践打下坚实的基础。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值