一、概要
- 本章论述了并发编程,介绍了并行计算的概念,指出了并行计算的重要性;比较了顺序算法与并行算法,以及并行性与并发性;解释了线程的原理及其相对于进程的优势;通过示例介绍了 Pthread 中的线程操作,包括线程管理函数,互斥量、连接、条件变量和屏障等线程同步工具;通过具体示例演示了如何使用线程进行并发编程,包括矩阵计算、快速排序和用并发线程求解线性方程组等方法;解释了死锁问题,并说明了如何防止并发程序中的死锁问题;讨论了信号量,并论证了它们相对于条件变量的优点;还解释了支持 Linux 中线程的独特方式。编程项目是为了实现用户级线程。它提供了一个基础系统来帮助读者开始工作。这个基础系统支持并发任务的动态创建、执行和终止,相当于在某个进程的同一地址空间中执行线程。
二、知识点总结
4.1 并行计算导论
1、顺序算法与并行算法
顺序算法:所有步骤通过单个任务依次执行,每次执行一个步骤。当所有步骤执行完成,算法结束。
并行算法:所有任务并行执行,所有任务完成后执行下一步。
2、并行性与并发性
在单CPU系统中,一次只能执行一个任务。在这种情况下,不同的热舞执行并发执行,即在逻辑上并行执行。在单周期CPU系统中,并发性是通过多任务处理来实现的。
4.2 线程
1、线程的原理
线程是某进程同一地址空间上的独立执行单元。
2、线程的优点
(1)线程创建和切换速度更快。
(2)线程的相应速度更快。
(3)线程更适合并行计算
3、线程的缺点
(1)由于地址空间共享,线程需要来自用户的明确同步。
(2)许多库函数可能对线程不安全。
(3)在单CPU系统上,使用线程解决问题实际上要比使用顺序程序慢,这是由在运行时创建线程和切换上下文的系统开销造成的。
4、线程管理函数
(1)创建线程
使用pthread_create()函数创建线程。
int prhread_create (pthread_t *pthread_id,pthread_attr_t *attr,
Void *(*func)(void *), void *arg);
如果成功则返回0,如果失败则返回错误代码。
其中,attr参数最复杂。下面给出了 attr参数的使用步骤。
1.定义一个pthread属性变量 pthread_attr_t attr。
2.用pthread_attr_init(&attr)初始化属性变量。
3.设置属性变量并在 pthread_create()调用中使用。
4.必要时,通过 pthread_attr_destroy(&attr)释放 attr资源。
4.线程同步
(1)互斥量
1.一种是静态方法:
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER
定义互斥量m,并使用默认属性对其进行初始化。
2.一种是动态方法:使用pthread_mutex_init()函数,可通过attr参数设置互斥属性。
pthread_mutex_init(pthread_mutex_t *m,pthread_mutexattr_t,*attr);
(2)死锁预防
有多种方法可以解决可能的死锁问题,其中包括死锁预防、死锁规避、死锁检测和恢复等。在实际系统中,唯一可行的方法是死锁预防,试图在设计并行算法时防止死锁的发生。一种简单的死锁预防方法是对互斥量进行排序,并确保每个线程只在一个方向请求互斥量,这样请求序列中就不会有循环。
但是,仅使用单向加锁请求来设计每个并行算法是不可能的。在这种情况下,可以使用条件加锁函数 pthread mutex trylock(来预防死锁。如果互斥量已被加锁,则 trylock(函数会立即返回一个错误。在这种情况下,调用线程可能会释放它已经获取的一些互斥量以便进行退避,从而让其他线程继续执行。在上面的交叉加锁示例中,我们可以重新设计其中一个线程,
避免死锁-利用银行家算法避免死锁。
(3)信号量
信号量是进程同步的一般机制。(计数)信号量是一种数据结构
struct sem{
int value; //semaphore(counter) value;
struct process *queue // a queue of blocked processes
}s;
在使用信号量之前,必须使用一个初始值和一个空等待队列进行初始化。不论是什么硬件平台,即无论是在单 CPU 系统还是多处理器系统上,信号量的低级实现保证了每次只能由一个执行实体操作每个信号量,并且从执行实体的角度来看,对信号量的操作都是(不可分割的)原子操作或基本操作。读者可以忽略这些细节,将重点放在信号量的高级操作及其作为进程同步机制的使用上。
(4)屏障
线程连接操作允许某线程(通常是主线程)等待其他线程终止。在等待的所有线程都终上后,主线程可创建新线程来继续执行并行程序的其余部分。创建新线程需要系统开销。在某些情况下,保持线程活动会更好,但应要求它们在所有线程都达到指定同步点之前不能继活动。在 Pthreads 中,可以采用的机制是屏障以及一系列屏障函数。首先,主线程创建一个屏障对象
pthread_barrier_t barrieri
并且调用
pthread_barrier_init(&barrier NULL,nthreads);
用屏障中同步的线程数字对它进行初始化。然后,主线程创建工作线程来执行任务。工作线程使用
pthread_barrier_wait( &barrier)
在屏障中等待指定数量的线程到达屏障。当最后一个线程到达屏障时,所有线程重新开始执行。在这种情况下,屏障是线程的集合点,而不是它们的坟墓。
实践与代码
例4.1:用线程计算矩阵的和
假设计算一各N*N整数矩阵中所有元素的和。
源代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define N 4
int A[N][N],sum[N];
void *func(void *arg)
{
int j,row ;
pthread_t tid = pthread_self();
row = (int)arg;
printf("Thread %d [%lu] computes sum of row %d
",row,tid,row);
for(j=0;j<N; j++)
sum[row] += A[row][j];
printf("Thread %d [%lu] done:sum [%d] =%d
",row,tid,row,sum[row]);
pthread_exit ((void*)0);
}
int main(int argc, char *argv[])
{
pthread_t thread[N];
int i,j,r,total = 0;
void *status;
printf("Main: initialize A matrix
");
for(i=0; i<N;i++){
sum[i] = 0;
for(j=0;j<N;j++){
A[i][j]=i*N+j+1;
printf("%4d ",A[i][j]);
}
printf( "
" );
}
printf ("Main: create %d threads
",N);
for(i=0;i<N;i++) {
pthread_create(&thread[i],NULL,func,(void *)i);
}
printf("Main: try to join with thread
");
for(i=0; i<N; i++) {
pthread_join(thread[i],&status);
printf("Main: joined with %d [%lu]: status=%d
",i,thread[i],
(int)status);
}
printf("Main: compute and print total sum:");
for(i=0;i<N;i++)
total += sum[i];
printf ("tatal = %d
",total );
pthread_exit(NULL);
}
主线程会先生成一个N×N整数矩阵。然后,它会创建N个工作线程,将唯一行号作为参数传递给各工作线程,并等待所有工作线程终止。每个工作线程计算不同行的部分和,并将部分和存入全局数组 int sum[N]的相应行中。当所有工作线程计算完成后,主线程继续进行计算。它将工作线程生成的部分和相加来计算总和。
运行结果
四、问题与解决
1、生产者消费者模型的作用是什么?
这个问题很理论,但是很重要:
(1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用
(2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约。
2、怎么检测一个线程是否拥有锁?
在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。