并发编程

逻辑控制流在时间上重叠,就称为并发。(concurrency)出现在计算机系统的多个层面上, 硬件异常处理, 进程和Unix信号处理程序

应用场景

访问慢速I/O设备, 应用需要等待I/O设备的数据到达时候,内核会运行其他进程,使得CPU保持繁忙。每个应用都可以按照类似的方式,通过交替执行I/O请求和其他有用的工作进行并发

服务多个网络客户端: 为每个客户端创建创建一个单独的控制流,允许服务器同时服务多个客户端服务。避免慢速I/O操作独占服务器

多个内核机器上的并行计算

并发的基本方法

进程: 每个逻辑控制流都是一个进程, 有内核进行维护调度,进程间通讯通过使用显示的进程间通讯(IPC) 机制

I/O多路复用: 应用程序在一个进程的上下文中显示的调度他们自己的逻辑控制流,逻辑流被模型化为状态机,数据达到文件描述符后,主程序显示的从一个状态转换到另一个状态,因为程序是一个单独的进程,所以所有的流都共享一个地址空间

线程: 是运行在一个单一进程上下文中的逻辑流,想进程流一样有内核进行调度,像I/O多路复用一样 共享同一个虚拟地址。

并发编程

基于进程的并发编程

服务器 接受请求之后,父进程fork一个子进程。子进程获得服务器描述符表的完整拷贝。

子进程关闭他拷贝的监听描述符, 父进程关闭他的已连接描述符, 因为子父子进程描述符指向同一个文件表表项,所以父进程关闭已连接描述符 是至关重要, 否则, 将永远不会释放已连接描述符的文件表条目,由此引发的存储器泄露 将最终消耗尽所有的存储器,导致系统崩溃 (为什么没有说 子进程关闭 监听描述符呢?以为子进程总是早于 父进程死掉,所以总是可以释放?)

父进程回收子进程

优劣

* 非常清晰的并发模型: 共享文件表,不共享用户地址空间。

* 独立的地址空间容易 使得进程共享状态信息变得更加困难。 为了共享信息,需要使用IPC机制, 2. 慢, 进程控制和IPC 的开销太高

I/O 多路复用并发编程

用来做 并发事件驱动(event-driven) 程序的基础, 在事件驱动程序中,流是因为某种事件而前进的。

I/O并发模型中的 逻辑流模型 转换为状态, 不严格的说, 一个状态机就是一组状态(state), 输入事件(input event), 和转移(transitI/On) 其中转移就是将输入事件和状态映射到另一个状态。

自循环(self loop) 是同一个输入和输出状态之间的转移。

服务器使用I/O 多路复用。 select 函数检测输入事件的发生。 当一个已连接描述符准备好读取的时候,服务器作为响应的状态机 执行转移

优点:

1. 比基于进程设计的设计给了程序员更多的对程序行为的控制,

2. 事件驱动服务器运行在单一的进程上下文中, 因为每个逻辑控制流都能否访问该进程的全部地址空间。这是的在流之间 共享数据变得容易

3. 调试起来变得简单,就像顺序程序一样

4. 事件驱动 常常比基于进程的设计的要高效的多,因为他们不需要进程上下文的调度

缺点:

1. 编码复杂, 不幸的是, 随着并发粒度的减小, 复杂性还会上升,

2. 不能够充分利用多核处理器

基于线程的并发模型

1. 概念

是运行在进程上下文中的逻辑流, 有内核自动调度,每个线程都有自己的线程上下文, 包括一个唯一的整数线程ID(thread ID TID), 栈,栈指针,程序计数器,etc,

基于线程的并发模型是结合 进程、I/O多路复用流的特性。 同进程一样内核自动调度, 同I/O复用一样, 多个线程运行在单一进程的上下文, 因此共享相同的虚拟地址空间, 代码,数据等

主线程: 每个进程开始生命周期时候是单一线程,这个线程成为主线程。 然后创建对等线程。 这个时间点开始,两个线程开始并发的执行。然后被 系统进行调度

与进程的不同: 线程的上线文要比进程的上下文小得多。不严格按照父子层次来组织。和一个进程相关的线程组成一个对等(线程池),主线程和其他线程的区别在于他总是进程中的第一个运行的线程,对等线程池的概念的主要影响是: 一个线程可以杀死他的任何对等进程,或者等待他的任何对等线程终止,每个对等线程都能读写相同的共享数据。

结合、分离: 在任何一个时间点, 线程是可结合的, 或者是分离的。一个可结合的线程能够被其他线程收回其他资源和杀死。在被其他线程回收之前,他的存储器资源是没有倍释放的。相反。 一个分离的线程是不能被其他线程回收或杀死的。他的存储资源在它终止由系统自动释放。 所以要么显示的回收,要么 pthread_join , pthread_detach. 在现实中,很好的理由要使用分离线程, web浏览器请求时都创建一个新的对等线程, 因为每个连接都是有一个单独的线程独立处理的。 对于服务器而言,就没有必要等待所有的对等线程终止,所以直接使用分离线程,在他们终止之后,资源就自动回收了。

2. 多线程序中的共享变量

线程存储模型: 线程都有自己的线程上下文, 线程ID, 栈, 栈指针, 程序计数器,条件码 和通用的寄存器,每个线程和其他线程一起共享进程上下文中的其他部分,包括用户虚拟地址空间(制度文本代码, 读写数据,堆,已经所有的共享库代码和数据区域)也同样共享打开的文件集合。

变量映射到线程

1. 全局变量: 定义在函数之外,只有一个实例, 任何线程都可以引用。

2. 本地自动变量: 定义在函数内,没有static的变量。每个线程栈都包含它自己的所有本地变量的实例。

3. 本地静态变量: 函数内部static变量。多个线程共享。

3. 同步线程 信号量

为了共享全局数据结构的并发程序的正确执行。

P(s) : s != 0 那么将s - 1 并返回, 如果s == 0 那么挂起这个线程,直到等待V操作会重启这个线程, p操作继续将S-1,执行。

V(s) : 将s + 1 , 重启任何一个阻塞的线程。

使用信号量, 实现互斥。 应用, 生产者— 消费者, 读者—写者

4.并发问题

线程安全

1. 不保护共享变量的函数: 全局变量, static变量

2. 保持调用状态的函数, 例如rand函数不是线程安全的。当前调用结果依赖前次调用的中间结果, 使rand函数线程安全的方法是, 重写他,使其不在依赖static变量。

3. 返回指向静态变量的函数

4. 调用线程不安全函数的函数

可重入函数

1. 特点在于被多个线程调用时,不会引用任何共享数据,可重入是线程安全的 真子集。

2. 可重入函数通常要比线程安全的函数要高效一点: 因为他们不需要同步操作,

竞争,死锁

5. 基于预线程化的并发服务器: 通过生产者消费者一个模型,

服务器是有一个主线程和一组工作者线程构成的

主线程不断的接受来自客户端的连接请求,并讲的到的连接请求描述符放到一个缓冲区中

每一个工作者线程 反复的从共享缓冲区中消费描述符

6. 使用线程提高并行性: 操作系统内核在多个核上并行的调度这些并发线程。并行程序常常被写为 每个核上只运行一个线程

总结

进程由内核调度,因为拥有独立的虚拟地址空间, 所以需要显示的IPC机制,来实现共享数据,同时 编程模型简单一致

事件驱动 使用I/O多路复用 显示的调度 并发逻辑流。以为在同一个进程中,所以共享数据变得简单, 复杂度比较高 (只能是单核应用了,但是能够高效利用IO,)

线程是两个的结合, 能够充分利用多核优势。但是调用函数,必须具有一种成为线程安全的性质。(信息同步,信号量,线程安全函数, 使得编程起来比较困难)

无论那种并发机制,同步对共享数据的访问都是一个困难的问题

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180316G0NP8B00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券