第四章 并发编程
4.1 并行计算导论
在早期,大多数计算机只有一个处理组件,称为处理器或中央处理器(CPU)。受这种硬件条件的限制,计算机程序通常是为串行计算编写的。要求解某个问题,先要设计一种算法,描述如何一步步地解决问题,然后用计算机程序以串行指令流的形式实现该算法。在只有一个CPU的情况下,每次只能按顺序执行某算法的一个指令和步骤。但是,基于分治原则(如二叉树查找和快速排序等)的算法经常表现出高度的并行性,可通过使用并行或并发执行来提高计算速度。并行计算是一种计算方案,它尝试使用多个执行并行算法的处理器更快速地解决问题。
4.1.1 顺序算法与并行算法
顺序算法:begin-end代码块中的顺序算法可能包含多个步骤。所有步骤都是通过单个任务依次执行的,每次执行一个步骤。当所有步骤执行完成时,算法结束。
begin step_1 step_2 …… step_n end //next step
并行算法:cobegin-coend代码块来指定并行算法的独立任务。在cobegin-coend块中,所有任务都是并行执行的。紧接着cobegin-coend代码块的下一个步骤将只在所有这些任务完成之后执行。
cobegin task_1 task_2 …… task_n coend //next step
4.1.2 并行性与并发性
在单CPU系统中,一次只能执行一个任务。在这种情况下,不同的任务只能并发执行,即在逻辑上并行执行。在CPU系统中,并发性是通过多任务处理来实现的。
4.2 线程
PCB1相当于主线程,新线程PCB2、PCB3、PCB4相当于用vfork创建出来的,它们指向同一块地址空间,它们隶属于同一个进程,但是他们有着自己的线程ID
4.2.1 线程的原理
- 一个操作系统(OS)包含许多并发进程。在进程模型中,进程是独立的执行单元。所有进程均在内核模式或用户模式下执行。
- 在内核模式下,各进程在唯一地址空间上执行,与其他进程是分开的。虽然每个进程都是一个独立的单元,但是它只有一个执行路径。当某进程必须等待某事件时,例如I/O完成事件,它就会暂停,整个进程会停止执行。
- 线程是某进程同一地址空间上的独立执行单元。创建某个进程就是在一个唯一地址空间创建一个主线程。
- 当某进程开始时,就会执行该进程的主线程。如果只有一个主线程,那么进程和线程实际上并没有区别。但是,主线程可能会创建其他线程。每个线程又可以创建更多的线程等。某进程的所有线程都在该进程的相同地址空间中执行,但每个线程都是一个独立的执行单元。
- 在线程模型中,如果一个线程被挂起,其他线程可以继续执行。除了共享共同的地址空间之外,线程还共享进程的许多其他资源,如用户 id,打开的文件描述符和信号等。
- 打个简单的比方,进程是一个有房屋管理员(主线程)的房子。线程是住在进程房子里的人。房子里的每个人都可以独立做自己的事情,但是他们会共用一些公用设施,比如同一个信箱、厨房和浴室等。
4.2.2 线程的优点
- 线程创建和切换的速度更快。
- 线程的响应速度更快。
- 线程更适合并行计算。
4.2.3 线程的缺点
- 由于地址空间共享,线程需要来自用户的明确同步。
- 许多库函数可能对线程不安全。任何使用全局变量或依赖于静态内存内容的函数,线程都不安全。为了使库函数适应线程环境,还需要做大量的工作。
- 在单CPU系统上,使用线程解决问题实际上要比使用顺序程序慢,这是由在运行时创建线程和切换上下文的系统开销造成的。
4.3 线程操作
- 线程的执行轨迹与进程类似。线程可在内核模式或用户模式下执行。
- 在用户模式下,线程在进程的相同地址空间中执行,但每个线程都有自己的执行堆栈。
- 线程是独立的执行单元,可根据操作系统内核的调度策略,对内核进行系统调用,变为挂起、激活以继续执行等。
- 为了利用线程的共享地址空间,操作系统内核的调度策略可能会优先选择同一进程中的线程,而不是不同进程中的线程。
4.4 线程示例函数
4.4.1 创建线程
使用pthread_create()函数创建线程:成功返回0
int pthread_create ( pthread_t *pthread_id, pthread_attr_t *attr, void *(*func)(void *), void *arg);
- pthread_id是指向pthread_t类型的指针。它会被操作系统内核分配的唯一线程ID填充。线程可通过pthread_self()函数来获得自己的ID,比如:
pthread_t tid=pthread_self()
。在Linux中,pthread_t类型被定义为无符号长整型,因此线程ID可以打印为%lu。 - attr是另一种不透明数据类型的指针,它指定线程属性。如果attr参数为NULL,将使用默认属性创建线程。
- func是要执行的新线程函数入口地址。
- arg是指向线程函数参数的指针,可写为 void *func(void *arg)。
4.4.2 线程ID
线程ID是一种不透明的数据类型,取决于实际情况。使用pthread_equal()函数可以对线程ID进行比较。
int pthread_equal(pthread_t t1,pthread_t t2);
不同线程返回0,否则返回非0。
4.4.3 线程终止
线程函数结束后,线程即终止。或者线程可以调用函数
int pthread_exit (void *status);
进行显示终止,其中状态是线程的退出状态。
返回0为正常终止,返回非0值为异常终止。
4.4.4 线程连接
一个线程可以等待另一个线程的终止,通过:
int pthread_join (pthread_t thread, void **status_ptr);
终止线程的退出状态以status_ptr返回。
4.5 线程示例程序
4.6 线程同步
向ChatGpt请求苏格拉底式询问