并发编程

本章论述了并发编程,介绍了并行计算的概念,指岀了并行计算的重要性;比较了顺序 算法与并行算法,以及并行性与并发性;解释了线程的原理及其相对于进程的优势;通过示 例介绍了 Pthread中的线程操作,包括线程管理函数,互斥量、连接、条件变量和屏障等线 程同步工具;通过具体示例演示了如何使用线程进行并发编程,包括矩阵计算、快速排序和 用并发线程求解线性方程组等方法;解释了死锁问题,并说明了如何防止并发程序中的死锁 问题;讨论了信号量,并论证了它们相对于条件变量的优点;还解释了支持Linux中线程的 独特方式。

并行计算导论

基于分治 原则(如二叉树查找和快速排序等)的算法经常表现出高度的并行性,可通过使用并行或并 发执行来提高计算速度。并行计算是一种计算方案,它尝试使用多个执行并行算法的处理器 更快速地解决问题。

并行性与并发性

并行算法只识别可并行执行的任务,但是它没有规定如何将任务映射到处理组件。在理想情况下,并行算法中的所有任务都应该同时实时执行。真正的并行执行只在有多个处理组件的系统中实现,比如多处理器或多核系统。在单CPU系统中,一次只能执行一个任务。在这种情况下,不同的任务只能并发执行,即在逻辑上并行执行。在单 CPU系统中,并发性是通过多任务处理来实现的

线程

原理

在进程模型中,进程是独立的执行单元。所 有进程均在内核模式或用户模式下执行。在内核模式下,各进程在唯一地址空间上执行,与 其他进程是分开的。虽然每个进程都是一个独立的单元,但是它只有一个执行路径。当某进 程必须等待某事件时,例如I/O完成事件,它就会暂停,整个进程会停止执行匸线程是某进 程同一地址空间上的独立执行单元。

某进程的所有线程都在该进程的相同地址空间中执行,但每个线程都是一个独立的执行单 元。在线程模型中,如果一个线程被挂起,其他线程可以继续执行。

优点

  1. 线程创建和切换速度更快
  2. 线程的响应速度更快
  3. 线程更适合并行计算

缺点

  1. 由于地址空间共享,线程需要来自用户的明确同步。
  2. 许多库函数可能对线程不安全
  3. 在单CPU系统上,使用线程解决问题实际上要比使用顺序程序慢,这是由在运行 时创建线程和切换上下文的系统开销造成的。

线程操作

线程可在内核模式或用户模式下执行 在用户模式下,线 程在进程的相同地址空间中执行,但每个线程都有自己的执行堆栈。线程是独立的执行単 元,可根据操作系统内核的调度策略,对内核进行系统调用,变为挂起、激活以继续执行等。

操作系统内核的调度策略可能会优先选择同-进程中的 线程,而不是不同进程中的线程。截至目前,几乎所有的操作系统都支持POSIX 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

创建线程

pthread_create()函数创建线程

int pthread_create (pthread_t *pthread_id, pthread_attr_t *attr, void *(*func)(void *), void *arg);
//pthread_id是指向pthread_t类型变量的指针。它会被操作系统内核分配的唯一线程 ID填充。在POSIX中,pthread_t是一种不透明的类型。程序员应该不知道不透明 对象的内容,因为它可能取决于实现情况。线程可通过pthread_self()函数获得自己 的ID。在Linux中,pthreadj类型被定义为无符号长整型,因此线程1D可以打印 为%111。
・attr是指向另一种不透明数据类型的指针,它指定线程属性,下面将对此进行更详细 的说明。
・fimc是要执行的新线程函数的入口地址。
・arg是指向线程函数参数的指针,可写为:
void *func(void *arg)

每个线程都使用默认堆栈的大小来创建。在执行过程中,线程可通过函数找到它的堆栈 大小:

size_t pthread_attr_getstacksize()

创建具有特定堆栈大小的线程

pthread_attr_t attr; 
size_t stacksize; 
pthread_attr_init(&attr); 
stacksize = 0x10000; 
pthread_attr_setstacksize(ftattr, stacksize);
pthread_create(&threads[t], &attr, func, NULL); 
//实际上,这是创建线程的建议 方法,除非有必要更改线程属性,否则应该遵循这种方法
。接下来,我们将attr设置为 NULL,就可始终使用默认属性。

线程 ID

pthread_equal()函数来比较

int pthread_equal (pthread_t tl, pthread_t t2);

线程终止

线程可以调用函数

int pthread_exit (void *status);

来显式终止

线程连接

一个线程可以等待另一个线程的终止,通过:

int pthread_join (pthread_t thread, void **status_ptr);

终止线程的退出状态以status_ptr返回。

线程同步

由于线程在进程的同一地址空间中执行,它们共享同一地址空间中的所有全局变量和数 据结构。当多个线程试图修改同一共享变量或数据结构时,如果修改结果取决于线程的执行 顺序,则称之为竞态条件。在并发程序中,绝不能有竞态条件。否则,结果可能不一致。

互斥量

互斥变量是用pthread_mutex_t类型声明的,在使 用之前必须对它们进行初始化。

  1. 静态方法
 pthread_rautex_t m = PTHREAD_MUTEX_INITIALIZER;
  1. 动态方法

使用pthread_mutex_init()函数,可通过attr参数设置互斥属

性,

pthread_mutex_init (pthread_mutex_t *m, pthread_mutexattr_t,*attr);

初始化完成后,线程可通过以下函数使用互斥量。

int pthread_mutex_lock (pthread_mutex_t *m);
int pthread_mutex_unlock (pthread mutex t *m);
int pthread_mutex_trylock (pthread_ioutex_t *m);
int pthread_mutex_destroy (pthread_mutex_t *m);

线程使用互斥量来保护共享数据对象互斥量的典型用法

pthread_mutex_lock(&m) ;// lock mutex
access shared data object; // access shared data in a critical region 
pthread_mutex_unlock;     // unlock mutex

死锁预防

假设某线程T1获取了互斥量ml,并且试图加锁另一个互斥量m2。另一个线 程T2获取了互斥量m2,并且试图加锁互斥量ml,在这种情况下,T1和T2将永远相互等待,由于交叉加锁请求,它们处于死锁状态。与竞态 条件类似,死锁决不能存在于并发程序中。

仅使用单向加锁请求来设计每个并行算法是不可能的。在这种情况下,可以使用 条件加锁函数pthread_mutex_trylock()来预防死锁。如果互斥量已被加锁,则trylock()函数 会立即返回一个错误。在这种情况下,调用线程可能会释放它已经获取的一些互斥量以便进 行退避,从而让其他线程继续执行。

条件变量

作为锁,互斥量仅用于确保线程只能互斥地访问临界区中的共享数据对象。条件变量提
供了一种线程协作的方法。条件变量总是与互斥量一起使用。

  • 静态方法
 pthread_cond_t con = PTHREAD_COND_INITIALIZER;
  • 动态方法
pthread_mutex_t con_mutex; // mutex for a condition variable
pthread_cond_t con;	// a condition variable that relies on con_mutex
pthread_mutex_init(&con_mutex, NULL);	// initialize mutex
pthread_cond_init(&conr NULL);	// initialize con

当使用条件变量时,线程必须先获取相关的互斥量,然后,它在互斥最的临界区内执行 操作,然后释放互斥量,

pthread_mutex_lock(&con_mutex);
modify or test shared data objects
use condition variable con to wait or signal conditions
pthread_mutex_unlock(&con_mutex);

生产者-消费者问题

一系列生产者和消费者进程共享数量有限的缓冲区。每个缓冲区每次有一个特定的项 目。最开始,所有缓冲区都是空的。当一个生产者将一个项目放入一个空缓冲区时,该缓冲 区就会变满。当一个消费者从一个满的缓冲区中获取一个项目时,该缓冲区就会变空。如果 没有空缓冲区,生产者必须等待。同样,如果没有满缓冲区,则消费者必须等待。此外,当 等待事件发生时.必须允许等待进程继续

实验代码及截图

我完成的是4.5.1用线程计算矩阵的和