12并发编程

并发编程

本章的核心内容:

  1. 实现并发的三种方式(进程、IO复用、线程),以及他们的优缺点
  2. 如何使用信号量来处理资源互斥、共享问题
  3. 认识不安全和死锁的概念

并发在哪些情况下有用?

  • 访问慢速I/O设备。
  • 与人交互。
  • 通过推迟工作以降低延迟。
  • 服务多个网络客户端。
  • 在多核机器上进行并行计算。

实现并发有哪些方式?

  • 进程。用这种方法,每个逻辑控制流都是一个进程,由内核来调度和维护。因为进程有独立的虚拟地址空间,想要和其他流通信,控制流必须使用某种显式的进程间通信机制。
  • I/O多路复用。在这种形式的并发编程中,应用程序在一个进程的上下文中显式地调度它们自己的逻辑流。逻辑流被模型化为状态机,数据到达文件描述符后,主程序显式地从一个状态转换到另一个状态。因为程序是一个单进程,所以所有的流都共享同一地址空间。
  • 线程。线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度。你可以把线程看成是其他两种方式的混合体,像进程流一样由内核进行调度,而像I/O多路复用流一样共享同一虚拟地址空间。

基于进程的并发编程

如何工作?

构造并发编程最简单的方法就是用进程,使用那些大家都很熟悉的函数,像fork、exec和waitpid。

步骤:

  1. 服务器监听一个监听描述符上的连接请求。
  2. 服务器接受了客户端1的连接请求,并返回一个已连接描述符。
  3. 在接受了连接请求之后,服务器派生一个子进程,这个子进程获得服务器描述符表的完整拷贝。子进程关闭它的拷贝中的监听描述符3,而父进程关闭它的已连接描述符4的拷贝,因为不再需要这些描述符了。
  4. 子进程正忙于为客户端提供服务,父进程继续监听新的请求。

基于进程的并发编程

基于进程的并发编程

注意点:

  1. 首先,通常服务器会运行很长的时间,所以我们必须要包括一个SIGCHLD处理程序,来回收僵死子进程的资源。因为当SIGCHLD处理程序执行时,SIGCHLD信号时阻塞的,而Unix信号时不排队的,所以SIGCHLD处理程序必须准备好回收多个僵死子进程的资源。
  2. 其次,子进程必须关闭它们各自的connfd拷贝。就像我们已经提到过的,这对父进程而言尤为重要,它必须关闭它的已连接描述符,以避免存储器泄漏。
  3. 最后,因为套接字的文件表表项中的引用计数,直到父子进程的connfd都关闭了,到客户端的连接才会终止。

优缺点?

共享文件表,但是不共享用户地址空间。

  • 优点:不会出现“一个进程不可能不小心覆盖另一个进程的虚拟存储器”的问题
  • 缺点:独立的地址空间使得进程共享状态信息变得更加困难。为了共享信息,它们必须使用显式的IPC(进程间通信)机制。基于进程的设计的另一个缺点是,它们往往比较慢,因为进程控制和IPC的开销很高。

案例:
基于进程的并发编程

基于I/O多路复用的并发编程

什么是I/O多路复用?

Unix网络编程给IO分了5种IO模型,其中包含阻塞IO、非阻塞IO、IO多路复用:

  • 阻塞IO:进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。
  • 非阻塞IO:进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。
  • IO多路复用: 多个的进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该select, select会监听所有注册进来的IO;

更多详细参见:IO模型

什么是事件驱动程序?

事件推动逻辑的执行,一般可以模型化状态机。

工作方式?

使用select

O多路复用的并发编程

优点:

  1. 使用事件驱动编程,这样比基于进程的设计给了程序更多的对程序行为的控制。
  2. 一个基于I/O多路复用的事件驱动服务器是运行在单一进程上下文中的,因此每个逻辑流都访问该进程的全部地址空间。这使得在流之间共享数据变得很容易

缺点:

  1. 事件驱动设计的一个明星的缺点就是编码复杂。我们的事件驱动的并发服务器需要比基于进程的多三倍。
  2. 不能充分利用多核处理器。

基于线程的并发编程

使用线程的好处?

  1. 因为一个线程的上下文要比一个进程的上下文小很多,线程的上下文切换要比进程的上下文切换快得多。
  2. 每个对等线程都能读写相同的共享数据

线程并发执行

api:

  • pthread_create():创建一个线程
  • pthread_exit():终止当前线程
  • pthread_cancel():请求中断另外一个线程的运行。
  • pthread_join():阻塞当前的线程,直到另外一个线程运行结束
  • pthread_detach():分离线程,分离后不需要被其他线程回收,终止时自行回收

案例:

线程并发执行

多线程程序中的共享变量

线程的基础内存内存模型是什么?

一组并发线程运行在一个进程的上下文中。 每个线程都有它自己独自的线程上下文,包括线程ID、栈、栈指针、程序计数器、条件码和通用目的寄存器值。每个线程和其他线程一起共享进程上下文的剩余部分。这包括整个用户虚拟地址空间,它是由只读文本(代码)、读/写数据、堆以及所有的共享库代码和数据区域组成的。线程也共享同样的打开文件的集合。

各自独立的线程栈的存储器模型不是那么整齐清楚的。这些栈被保存在虚拟地址空间的栈区域中,并且通常是被相应的线程独立地访问的。我们说通常而不是总是,是因为不同的线程栈是不对其他线程设防的。所以,如果一个线程以某种方式得到一个指向其他线程栈的指针,那么它就可以读写这个栈的任何部分。

根据这个模型,变量实例是如何映射到内存的?

  • 全局变量:定义在函数之外的变量。在运行时,虚拟存储器的读/写区域只包含每个全局变量的一个实例,任何线程都可以引用。
  • 本地自动变量:定义在函数内部但是没有static属性的变量。在运行时,每个线程的栈都包含它自己的所有本地自动变量的实例。即使当多个线程执行同一个线程例程时也是如此。
  • 本地静态变量:定义在函数内部并有static属性的变量。和全局变量一样,虚拟存储器的读/写区域只包含在程序中声明的每个本地静态变量的一个实例。

有多少线程引用这些实例?

一个变量是共享的,当且仅当它的一个实例被一个以上的线程引用。

用信号量同步线程

共享变量带来的问题?

共享变量引入了同步错误。

譬如下面一段代码:i++(注意这不是一个原子操作)

同步问题代码

同步问题

如何分析同步错误的问题?

可以使用线程进度图来分析,如下:

进度图

对每个线程而言,操作共享变量的指令区间叫做临界区
两个临界区的交集称作不安全区

不安全区

什么是信号量?

信号量是一个非负整数,关键在于对信号量的操作(增减)是原子的

如何使用信号量解决同步问题?

只用0和1两种值的信号量叫二元信号量,也叫互斥锁
用互斥锁把临界区代码包起来

信号量

上面的代码使得线程的轨迹无法进入不安全区

禁止区

如何使用信号量来处理共享资源的访问问题?

信号量>1

生产者——消费者问题?

生产者——消费者问题

有限缓存抽象如下:
有限缓存

生产和消费方式如下:
生产——消费

读者——写者问题?

修改对象的线程叫做写者;只读对象的线程叫做读者。
写着必须拥有对对象的独占访问,而读者可以和无限多个其他读者共享对象。

分两类:

  • 读者优先,要求不要让读者等待,除非已经把使用对象的权限赋予了一个写者。
  • 写者优先,要求一定能写者准备好可以写,它就会尽可能地完成它的写操作。

第一类解决方案如下:

读者优先

什么是饥饿?

线程无限制阻塞,无法进展。比如上面的写者就有可能永远拿不到锁

综合:基于预线程化的并发服务器

什么是预线程化?

预线程化

案例:

使用线程提高并行性

顺序、并发、并行之间是什么关系?

顺序、并发、并行

多线程在多核下的性能一定更好吗?

多核下同步操作的性能消耗更大,因此要尽可能避免

线程越多性能越好吗?

刻画并行程序的性能

时间上升,是因为没有阻塞的环境下,一个核的多线程上下文切换导致
所以,并行程序常常是每个核一个线程

其他并发问题

什么是线程不安全的?

一个函数被称为线程安全的,当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果。如果一个函数不是线程安全的,我们就说它是线程不安全的。

线程不安全的函数类有哪些?**

  1. 不保护共享变量的函数。
  2. 保持跨越多个调用的状态的函数。如伪随机数生成器
  3. 返回指向静态变量的指针的函数。
  4. 调用线程不安全函数的函数。

解决方案:除了第二种必须修改源码外:其余的都可以采用“加锁-拷贝技术”:线程不安全函数与互斥锁联系起来,在每一个调用位置,对互斥锁加锁,调用线程不安全函数,将函数返回的结果拷贝到一个私有的存储器位置,然后对互斥锁解锁。

什么是可重入函数?和线程安全函数有啥区别?

当它们被多个线程调用时,不会引用共享数据。

可重入函数

如何通过线程进程图来判断死锁?

死锁进度图

避免死锁的加锁规则?

每个线程都是以相同的顺序获得互斥锁

上面结论有证实

本文标题:12并发编程

文章作者:Sun

发布时间:2019年09月16日 - 11:09

最后更新:2019年09月16日 - 11:09

原始链接:https://sunyi720.github.io/2019/09/16/系统原理/12并发编程/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。