2021-02-07

1、基本介绍

如果逻辑控制流在时间上是重叠的,那么它们就是并发的。应用级并发可以发生在:

  • 访问慢速I/O设备:当一个应用正在等待来自慢速I/O设备(例如磁盘)的数据到达时,内核会运行其他进程,使CPU保持繁忙。这是通过交替执行I/O请求和其他有用的工作来使用并发。
  • 与人交互:用户希望计算机有同时执行多个任务的能力。每次用户请求某种操作(如单击鼠标)时,一个独立的并发逻辑流被创建来执行这个操作。
  • 通过推迟工作来降低延迟。
  • 服务多个网络客户端:我们期望服务器每秒为成百上千的客户端提供服务,并发服务器为每个客户端创建一个单独的逻辑流。
  • 在多核机器上进行并行计算:被划分成并发流的应用程序通常在多核机器上比在单处理器上运行得快,因为这些流会并行执行,而不是交错执行。

现代操作系统提供了三种基本的构造并发程序的方法:

(1)进程:用这种方法,每个逻辑控制流都是一个进程,由内核来调度和维护。因为进程有独立的虚拟地址空间,想要和其他流通信,控制流必须使用某种显式的进程间通信(IPC)机制。

(2)I/O多路复用:在这种形式的并发编程中,应用程序在一个进程的上下文中显式地调度它们自己的逻辑流。逻辑流被模型化为状态机,数据到达文件描述符后,主程序显式地从一个状态转换为另一个状态。因为程序是一个单独的进程,所以所有的流都共享同一个地址空间。

(3)线程:线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度。可以把线程看成是其他两种方式的混合体,像进程流一样由内核进行调度,而像I/O多路复用流一样共享同一个虚拟地址空间。

2、基于进程的并发编程

2.1、基于进程的并发服务器

**构造并发程序最简单的方法就是用进程**,使用那些大家都熟悉的函数,像fork、exec和waitpid。例如,一个构造并发服务器的自然方法就是,在父进程中接收客户端连接请求,然后创建一个新的子进程来为每个新客户提供服务。为了理解这是如何工作的,假设我们有两个客户端和一个服务器,服务器正在监听一个监听描述符(比如描述符3)上的连接请求。现在假设服务器接受了客户端1的连接请求,并返回一个已连接描述符(比如描述符4),如图12-1。在接受连接请求之后,服务器派生一个子进程,这个子进程获得服务器描述表的完整副本。子进程关闭它的副本中整监听描述符3,而父进程关闭它的已连接描述符4的副本,因为不在需要这些描述符了。这就得到了图12-2的状态,其中子进程正忙于为客户端提供服务。
img

因为父、子进程中的已连接描述符都指向同一个文件表表项,所以父进程关闭它的已连接描述符的副本是至关重要的。否则,将永远不会释放已连接描述符4的文件表条目,而且由此引起的内存泄露将最终消耗光可用的内存,使系统奔溃。现在,假设在父进程为客户端1创建了子进程之后,它接受一个新的客户端2的连接请求,并返回一个新的已连接描述符(比如描述符5),如图12-3。然后,父进程又派生另一个子进程,这个子进程已连接描述符5为它的客户端提供服务,如图12-4.此时,父进程正在等待下一个连接请求,而两个子进程正在并发地为它们各自的客户端提供服务。

img

2.2、进程优劣

对于在父子进程间共享状态信息,进程有一个非常清晰的模型:共享文件表但是不共享用户地址空间。进程有独立的地址空间即使优点优点也是缺点。

(1)优点:有独立的地址空间,一个进程不可能不小心覆盖另一个进程的虚拟内存,这就消除了许多令人迷惑的错误。

(2)缺点:独立的地址空间使得进程共享状态信息变得更加困难。为了共享信息,它们必须使用显式的IPC(进程间通信)机制。

(3)缺点:基于进程的设计另一个缺点是,它们往往比较慢,因为进程控制和IPC的开销很高

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

3.1、基于I/O的多路复用的并发事件驱动服务器

假设要求编写一个echo服务器,它也能对用户从标准输入键入的交互命令做出响应。此时服务器必须响应两个互相独立的I/O事件:

1)网络客户端发起的连接请求

2)用户在键盘上键入的命令

解决的办法是I/O多路复用技术。基本思想是使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序

可以使用select、poll和epoll来实现I/O复用。

3.2、I/O多路复用技术的优劣

优点:

1)使用事件驱动编程,这样比基于进程的设计给了程序更多的对程序行为的控制。

2)一个基于I/O多路复用的事件驱动服务器是运行在单一进程上下文中的,因此每个逻辑流都访问该进程的全部地址空间。这使得在流之间共享数据变得很容易。一个与作为单进程运行相关的优点是,你可以利用熟悉的调试工具,例如GDB来调试你的并发服务器,就像对顺序程序那样。最后,事件驱动设计常常比基于进程的设计要高效很多,因为它们不需要进程上下文切换来调度新的流。

缺点:

1)事件驱动设计的一个明显缺点就是编码复杂。我们的事件驱动并发服务器需要的代码比基于进程的服务器多三倍。不幸的是,随着并发粒度的减小,复杂性还会上升。这里的粒度是指每个逻辑流每个时间片执行的指令数量

2)基于事件的设计的另一重大缺点是它们不能充分利用多核处理器。

4、基于线程的并发编程

4.1、线程执行模型

 多线程的执行模型在某些方面和多进程的执行模型是相似的。**每个进程开始生命周期时都是单一线程,这个线程是主线程**。在某一时刻,主线程创建一个对等线程,从这个时间点开始,两个线程就并发地运行。最后,因为主线程执行一个慢速系统调用,例如read和sleep,或者因为它被系统的间隔计时器中断,**控制就会通过上下文切换到对等线程。对等线程会执行一段时间,然后控制传递回主线程,依次类推**。

 在一些重要的方面,线程执行是不同于进程的。因为**一个线程的上下文要比一个进程的上下文小很多,线程的上下文切换要比进程的上下文切换快得多**。另一个不同就是线程不像进程那样,不是按照严格的父子层次来组织的。**和一个进程相关的线程组成一个对等(线程)池,独立于其他线程创建的线程**。主线程和其他线程的区别仅在于它总是进程中第一个运行的线程。对等(线程)池概念的主要影响是,**一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止**。另外,**每个对等线程都能读写相同的共享数据**。

在某一时刻,主线程创建一个对等线程,从在此刻开始,两个线程就并发地运行。

image

4.2、

4.3、创建线程

#include <pthread.h>

typedef void *(func)(void *);

//  返回: 若成功返回0,若出错则为非0
int pthread_create(pthread_t *tid, pthread_attr_t *attr, func *f, void *arg);
//  返回: 返回调用者的线程ID                    
pthread_t pthread_self(void);   

pthread_create: 创建一个新线程,arg是输入变量,在新线程上下文中运行线程例程f。atrr参数用于改变新线程的默认属性。tid用于返回新线程的ID
pthread_self: 获取线程自己的ID

4.4、终止线程

线程以下列方式来终止:

  • 线程例程返回时, 线程会隐式地终止
  • 通过调用pthread_exit函数,线程会显式地终止。若是主线程调用了pthread_exit,它会等待其他所有对等线程终止,然后再终止主线程和整个进程
  • 某个对等线程调用Unix的exit函数,该函数终止进程及该进程所有相关线程。
  • 另一个对等线程调用pthread_cancle(参数为当前线程ID),来终止当前线程。
#include <pthread.h>

// 返回: 若成功返回0,出错则为非0
int pthread_cancel(pthread_t tid);

4.5、回收已终止线程的资源

线程通过调用pthread_join函数等待其他线程终止

#include <pthread.h>
//   返回: 若成功返回0,出错则为非0
int pthread_join(pthread_t tid, void **thread_return);

pthread_join函数会阻塞,直到线程tid终止,将线程例程返回的(void*)指针赋值为pthread_return指向的位置,然后回收已终止线程占用的所有存储器资源.

和Unix的wait函数不同,pthread_join函数只能等待一个指定的线程终止,没有办法让pthread_join等待任意一个线程终止。

4.6、分离线程

线程是可结合的(joinable)或可分离的(detached)。

  • 可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(如栈)是没有被释放的
  • 一个分离的线程是不能被其他线程回收或杀死的,其存储器资源在它终止时由系统自动释放.

默认情况,线程被创建成可结合的。为避免存储器泄露,每个可结合线程都应该:

  • 要么被其他线程显式回收
  • 要么通过调用pthread_detach函数被分离。
#include <pthread.h>
// 返回: 若成功则返回0,出错则为非0
int pthread_detach(pthread_t tid);  
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 现代操作系统提供了三种基本的构造并发程序的方法: 1、进程: 每个逻辑控制流都是一个进程,由内核来调度和维护。因为...
    ShawnPanCn阅读 649评论 0 0
  • 第十二章、并发编程 现代操作系统提供了三种基本的构造并发程序的方法: 1、进程: 每个逻辑控制流都是一个进程,由内...
    wenmingxing阅读 594评论 0 3
  • 计算机系统漫游 代码从文本到可执行文件的过程(c语言示例):预处理阶段,处理 #inlcude , #defin...
    willdimagine阅读 3,694评论 0 5
  • 使用应用级并发的应用程序称为并发程序(concurrent program)。现代操作系统提供3种基本的构造并发程...
    Leon_Geo阅读 1,109评论 0 8
  • 操作系统 面向进程和线程学习操作系统。 目录 Chapter 1Chapter 2Chapter 3Chapter...
    java技术分享师阅读 194评论 0 0