20201322陈俊池学习笔记7

第四章 并发编程

4.1 并行计算导论

在早期,大多数计算机只有一个处理组件,称为处理器或中央处理器(CPU)。受这种硬件条件的限制,计算机程序通常是为串行计算编写的。要求解某个问题,先要设计一种算法,描述如何一步步地解决问题,然后用计算机程序以串行指令流的形式实现该算法。在只有一个CPU的情况下,每次只能按顺序执行某算法的一个指令和步骤。但是,基于分治原则(如二叉树查找和快速排序等)的算法经常表现出高度的并行性,可通过使用并行或并发执行来提高计算速度。并行计算是一种计算方案,它尝试使用多个执行并行算法的处理器更快速地解决问题。过去,由于并行计算对计算资源的大量需求,普通程序员很少能进行并行计算近年来,随着多核处理器的出现,大多数操作系统(如Linux)都支持对称多处理(SMP)。甚至对于普通程序员来说,并行计算也已经成为现实。显然,计算的未来发展方向 是并行计算-因此,迫切需要在计算机科学和计算机工程专业学生的早期学习阶段引入并行 计算。在本章中,我们将介绍通过并发编程实现并行计算的基本概念和方法。

4.1.1 顺序算法与并行算法

在描述顺序算法时,常用的方法是用一个begin-end代码块列出算法,如下图左侧所示。

begin-end代码块中的顺序算法可能包含多个步骤。所有步骤都是通过单个任务依次执行的, 每次执行一个步骤。当所有步骤执行完成时,算法结束。相反,图的右侧为并行算法的描述,它使用cobegin-coend代码块来指定并行算法的独立任务。在cobegin-coend块中.所有任务都是并行执行的,紧接着cobegin-coend代码块的下一个步骤将只在所有这些任务完成之后执行。

4.1.2 并行性与并发性

通常,并行算法只识别可并行执行的任务,但是它没有规定如何将任务映射到处理组件。在理想情况下,并行算法中的所有任务都应该同时实时执行。然而,真正的并行执行只曲在有多个处理组件的系统中实现,比如多处理器或多核系统。在单CPU系统中,一次只能执行一个任务。在这种情况下,不同的任务只能并发执行,即在逻辑上并行执行。在单CPU系统中,并发性是通过多任务处理来实现的,该内容已在第3章中讨论过。在本章的最后,我们将在一个编程项目中再次讲解和示范多任务处理的原理和方法。

4.2 线程

4.2.1 线程的原理

一个操作系统(OS)包含许多并发进程“在进程模型中,进程是独立的执行单元。所有进程均在内核模式或用户模式下执行。在内核模式下,各进程在唯一地址空间上执行,与其他进程是分开的。

虽然每个进程都是一个独立的单元,但是它只有一个执行路径。当某进程必须等待某事件时,例如I/O完成事件,它就会暂停,整个进程会停止执行匸线程是某进程同一地址空间上的独立执行单元。

创建某个进程就是在一个唯一地址空间创建一个主线程。当某进程开始时,就会执行该进程的主线程。如果只有一个主线程,那么进程和线程实际上并没有区别。但是,主线程可能会创建其他线程。每个线程又可以创建更多的线程等。

某进程的所有线程都在该进程的相同地址空间中执行,但每个线程都是一个独立的执行单元。在线程模型中,如果一个线程被挂起,其他线程可以继续执行。除了共享共同的地址空间之外.线程还共享进程的许多其他资源,如用户id、打开的文件描述符和信号等。

4.2.2 线程的优点

与进程相比,线程有许多优点。 (1)线程创建和切换速度更快:进程的上下文复杂而庞大。其复杂性主要来自管理进程映像的需要。在执行过程中.有些页面在内存中,有些则不在内存中。操作系统内核必须使用多个页表和多个级别的硬件辅助来跟踪每个进程的页面-要想创建新的进程,操作系统必须为进程分配内存并构建页表。若要在某个进程中创建线程,操作系统不必为新的线程分配内存和创建页表,因为线程与进程共用同一个地址空间。所以,创建线程比创建进程更快。另外.由于以下原因,线程切换比进程切换更快。进程切换涉及将一个进程的复杂分贞环境替换为另一个进程的复杂分页环境,需要大量的操作和时间。相比之下,同一个进程中的线程切换要简单得多、也快得多,因为操作系统内核只需要切换执行点,而不需要更改进程映像。 (2)线程的响应速度更快:一个进程只有一个执行路径。当某个进程被挂起时,整个进程都将停止执行。相反,当某个线程被挂起时,同进程中的其他线程可以继续执行。这使得有多个线程的程序响应速度更快。 (3)线程更适合并行计算:并行计算的目标是使用多个执行路径更快地解决问题。基于分治原则(如二叉树查找和快速排序等)的算法经常表现出高度的并行性,可通过使用并行或并发执行来提高计算速度。这种算法通常要求执行实体共享公用数据。在进程模型中,各进程不能有效共享数据,因为它们的地址空间都不一样。

4.23 线程的缺点

(1)由于地址空间共享,线程需要来自用户的明确同步。 (2)许多库函数可能对线程不安全,例如传统strtok()函数将一个字符串分成一连串令牌。通常,任何使用全局变量或依赖于静态内存内容的函数.线程都不安全。为了使库函数适应线程环境,还需要做大量的工作。 (3)在单CPU系统上,使用线程解决问题实际上要比使用顺序程序慢,这是由在运行时创建线程和切换上下文的系统开销造成的。

4.3 线程操作

线程的执行轨迹与进程类似。线程可在内核模式或用户模式下执行在用户模式下,线程在进程的相同地址空间中执行,但每个线程都有自己的执行堆栈。线程是独立的执行単元,可根据操作系统内核的调度策略,对内核进行系统调用,变为挂起、激活以继续执行等。为了利用线程的共享地址空间,操作系统内核的调度策略可能会优先选择同-进程中的线程,而不是不同进程中的线程。

4.4 线程管理函数

Pthread库提供了用于线程管理的以下API。

pthread_create(thread, attr, function, arg): create thread
pthread_exit(status) : terminate thread
pthread_cancel(thread) : cancel thread
pthread_attr_init(attr) : initialize thread attributes
pthread_attr_destroy(attr): destroy thread attribute

4.4.1 创建线程

使用pthread_create()函数创建线程。

int pthread_create (pthread_t *pthread_id, pthread_attr_t *attr, void *(*func)(void *), void *arg);

如果成功则返回0,如果失败则返回错误代码

4.4.2 线程ID

线程ID是一种不透明的数据类型,取决于实现情况。因此,不应该直接比较线程ID。如果需要,可以使用pthread_equal()函数对它们进行比较。

int pthread_equal (pthread_t tl, 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.6 线程同步

当多个线程试图修改同一共享变量或数据结构时,如果修改结果取决于线程的执行顺序,则称之为竞态条件。

4.6.1互斥量

在 Pthread中,锁被称为互斥量,意思是相互排斥。

互斥变呈是用

ptbread_mutex_t

类型声明的在使,用之前必须对它们进行初始化。

有两种方法可以初始化互斥址:

  1. 静态方法:pthreaa—mutex_t m = PTHREAD_MUTEX_INITIALIZER,定义互斥量 m, 并使用默认属性对其进行初始化。

  2. 动态方法,使用 pthread_ mutex _init() 函数

线程通过互斥量来保护共享数据对象.

4.6.2 死锁预防

死锁是一种状态,在这种状态下,许多执行实体相互等待,因此都无法继续下去。

死锁预防,试图在设计并行算法时防止死锁的发生。

一种简单的死锁预防方法是对互斥量进行排序,并确保每个线程只在一个方向请求互斥量,这样请求序列中就不会有循环。

4.6.3 条件变量

条件变量提供了一种线程协作的方法。

在Pthread中,使用类型pthread_cond_t来声明条件变量,而且必须在使用前进行初始化。

与互斥变量一样,条件变量也可以通过两种方法进行初始化。

静态方法:pthread_cond_t con= PTHREAD_COND_INITIALIZER;定义一个条件变屾con,并使用默认属性对其进行初始化。

动态方法:使用pthread_cond_init()函数,可通过attr参数设置条件变量。为简便起见,我们总是使用NULLattr参数作为默认属性。

实践内容与截图

代码:

    #include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
typedef struct{
int upperbound;
int lowerbound;
}PARM;
#define N 10
int a[N]={5,1,6,4,7,2,9,8,0,3};// unsorted data
int print(){//print current a[] contents
int i;
printf("[");
for(i=0;i<N;i++)
printf("%d ",a[i]);
printf("]\n");
}
void *Qsort(void *aptr){
PARM *ap, aleft, aright;
int pivot, pivotIndex,left, right,temp;
int upperbound,lowerbound;
pthread_t me,leftThread,rightThread;
me = pthread_self();
ap =(PARM *)aptr;
upperbound = ap->upperbound;
lowerbound = ap->lowerbound;
pivot = a[upperbound];//pick low pivot value
left = lowerbound - 1;//scan index from left side
right = upperbound;//scan index from right side
if(lowerbound >= upperbound)
pthread_exit (NULL);
while(left < right){//partition loop
do{left++;} while (a[left] < pivot);
do{right--;}while(a[right]>pivot);
if (left < right ) {
temp = a[left];a[left]=a[right];a[right] = temp;
}
}
print();
pivotIndex = left;//put pivot back
temp = a[pivotIndex] ;
a[pivotIndex] = pivot;
a[upperbound] = temp;
//start the "recursive threads"
aleft.upperbound = pivotIndex - 1;
aleft.lowerbound = lowerbound;
aright.upperbound = upperbound;
aright.lowerbound = pivotIndex + 1;
printf("%lu: create left and right threadsln", me) ;
pthread_create(&leftThread,NULL,Qsort,(void * )&aleft);
pthread_create(&rightThread,NULL,Qsort,(void *)&aright);
//wait for left and right threads to finish
pthread_join(leftThread,NULL);
pthread_join(rightThread, NULL);
printf("%lu: joined with left & right threads\n",me);
}
int main(int argc, char *argv[]){
PARM arg;
int i, *array;
pthread_t me,thread;
me = pthread_self( );
printf("main %lu: unsorted array = ", me);
print( ) ;
arg.upperbound = N-1;
arg. lowerbound = 0 ;
printf("main %lu create a thread to do QS\n" , me);
pthread_create(&thread,NULL,Qsort,(void * ) &arg);//wait for Qs thread to finish
pthread_join(thread,NULL);
printf ("main %lu sorted array = ", me);
print () ;
}

截图:

 

posted @ 2022-10-16 13:05  20201322陈俊池  阅读(18)  评论(0编辑  收藏  举报