一、程序框架设计
1.1 课题要求
1、使用多个队列,每一个计算线程有独立的队列用于存储计算请求,请求线程可用一个队列用于接收结果。
2、实现A(+,-,X,/)B简单两元计算。
3、请求线程与计算线程是多对多关系。
1.2 基本概念回顾
1.2.1 线程的定义
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组 成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程 在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
1.2.2 线程的特点
在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。
1)轻型实体
线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源,比如,在每个线程中都应具有一个用于控制线程运行的线程控制块TCB,用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
2)独立调度和分派的基本单位。
在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
3)可并发执行。
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
4)共享进程资源。
在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
1.2.3线程与进程的比较
进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中。以表示该进程拥有这些资源或正在使用它们。
另外,进程也是抢占处理机的调度单位,它拥有一个完整的虚拟地址空间。当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一地址空间。
与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其他线程一起共享进程的资源。
线程只由相关堆栈(系统栈或用户栈)寄存器和线程控制表TCB组成。寄存器可被用来存储线程内的局部变量,但不能存储其他线程的相关变量。
通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更 高效的提高系统内多个程序间并发执行的程度,从而显著提高系统资源的利用率和吞吐量。因而近年来推出的通用操作系统都引入了线程,以便进一步提高系统的并发性,并把它视为现代操作系统的一个重要指标。
线程与进程的区别可以归纳为以下4点:
1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
3)调度和切换:线程上下文切换比进程上下文切换要快得多。
4)在多线程OS中,进程不是一个可执行的实体。
1.2.4互斥锁
如果不需要信号量的计数能力,有时可以使用信号量的一个简化版本,称为互斥量(mutex)。互斥量仅仅适用于管理共享资源或一小段代码。由于互斥量在实现时既容易又有效,这使得互斥量在实现用户空间线程包时非常有用。
互斥量是一个可以处于两态之一的变量:解锁和加锁。这样,只需要一个二进制位表示它,不过实际上,常常使用一个整型量,0表示解锁,而其他所有的值则表示加锁。互斥量使用两个过程。当一个线程(或进程)需要访问临界区时,它调用mutex_lock。如果该互斥量当前是解锁的(即临界区可用),此调用成功,调用线程可以自由进入该临界区。
另一方面,如果该互斥量已经加锁,调用线程被阻塞,直到在临界区中的线程完成并调用mutex_unlock。如果多个线程被阻塞在该互斥量上,将随机选择一个线程并允许它获得锁。
由于互斥量非常简单,所以如果有可用的TSL或XCHG指令,就可以很容易地在用户空间中实现它们。
1.2.5
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
条件变量和互斥锁一样,都有静态和动态两种创建方式,静态方式使用PTHREAD_COND_INITIALIZER常量进行初始化,如下:
pthread_cond_tcond = PTHREAD_COND_INITIALIZER;
动态方式调用pthread_cond_init()函数,API定义如下:
int pthread_cond_init(pthread_cond_t*cond, pthread_condattr_t *cond_attr)
尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候,才能注销这个条件变量,否则返回EBUSY。API定义如下:
int pthread_cond_destroy(pthread_cond_t *cond)
且被忽略。
1.2.6线程中需要用的函数
POSIX线程(Pthreads)的头文件是<pthread.h>,适用于类Unix操作系统。
1.创建线程
int pthread_create(pthread_t*thread,constpthread_attr_t*attr,void*(*start_routine)(void*),void*arg);
2.等待线程
int pthread_join(pthread_tthread,void**retval);
3.退出线程
void pthread_exit(void*retval);
4.返回当前线程的线程标识符
pthread_t pthread_self(void);
5.线程取消
int pthread_cancel(pthread_tthread);
1.3 程序总体设计
本课题按照要求使用了多个队列,在设计每一个计算线程的时候,也设计了独立的队列用于存储计算请求,其中请求线程实现了用一个队列用于接收结果的功能。实现了A(+,-,X,/)B简单两元计算,请求线程与计算线程是多对多关系,总体基本实现了要求,经过测试,运行结果正常。程序总体框图如图所示。
图1 程序总体框图
总体框图的设计思想:首先主控线程先创建出N个请求线程,M个计算线程。这N个请求线程分别读指定的数据文件,将读到的计算请求放入总的请求队列中。有人会问在这里如何保证这多个线程之前不会产生竞争导致数据错乱,这里要特别说明线程之间共享着文件表项及文件描述符,又因为read操作是原子操作,从而在多线程读文件时不会出现不正常读取现象。但是需要主要的是在各个请求放入总请求队列时会发生竞争导致错乱,原因将在下面讲述到。在这里需要加入请求队列互斥量,有了互斥量可以很好的避免发生多线程竞争。M个计算线程需要考虑当总请求队列中有任务时要去读取请求,将它们放入自己的队列中,注意如果没有任务时,这些线程也会不断的去尝试读取队列,这里就要做一些处理。当队列为空时,可以让各个计算线程进入睡眠状态,然后一旦出现任务则激活这些计算线程。为了实现这种功能,我们加入了条件变量,但为了简化设计框图这里并没有把条件变量放上去,在后续的讨论中我们将详细的讲解。
二、核心数据结构
2.1 队列数据结构的设计与实现
对于队列的设计,是用链表式队列实现的。队列中的每个元素为一个任务结构体,该结构体包括下一个结构体的指针,计算的两个值a,b,还有决定何种运算的运算符。同时,为了方便构造存放计算结果的结构,我们添加了answer元素,这样该job结构既能表示每个任务请求,又能表示每个计算结果。队列数据结构设计的部分代码如下:
typedef struct Queue queue; //定义队列结构体
typedef struct Job job; //定义任务结构体
//任务结构体数据结构设计
struct Job {
struct Job *next;
int a;
intb;
char str[MAXLINE];
double answer;
};
//队列结构体数据结构设计
struct Queue {
struct Job *head;
struct Job *tail;
};
2.2 多线程运行情况下的数据结构设计
在多线程运行环境中,当多个线程同时对某个确定对象进行操作的时候,为了防止由于各个线程之间产生竞争关系我们必须适当加上一些锁。在我们的程序设计架构里可以看到,多线程会对总的请求队列和总的结构队列产生竞争操作,所以我们需要定义两个互斥量,分别为qlock与qlock2,这两个互斥量分别对于请求队列的锁与结构队列的锁。
程序运行过程中,每个线程会不断地去争夺请求队列锁,从而需要判断队列是否为空。在请求队列为空的情况下,如果每个线程仍然去不断地争夺互斥量这必将导致程序效果低下,同时产生很多不必要的阻塞。为了解决这个问题,我们采用了条件变量的办法,定义一个条件变量qready,当线程得知当前请求队列为空时就进入睡眠状态,等待请求队列不为空的条件变量去激活,这样就能有效的避免大量的线程阻塞。多线程运行情况下的数据结构设计的部分代码如下:
//定义qlock互斥变量
pthread_mutex_t qlock =PTHREAD_MUTEX_INITIALIZER;
//定义qlock2互斥变量
pthread_mutex_t qlock2 =PTHREAD_MUTEX_INITIALIZER;
//定义条件变量qready
pthread_cond_t qready =PTHREAD_COND_INITIALIZER;
三、异常情况设计
3.1产生多个请求队列出错
产生多个请求队列出错时,退出线程,并输出提示信息,部分代码如下。
if (pthread_create(&tid, NULL,pthr_req, (void *)i) != 0){
printf("create the %dth require thread error\n", i);
exit(0);
}
3.2 产生多个计算队列出错
产生多个计算队列出错时,退出线程,并输出提示信息,部分代码如下。
if (pthread_create(&tid, NULL,pthr_cal, (void *)i) != 0){
printf("create the %dth calculate thread error\n", i);
exit(0);
}
3.3 输入的参数有误
当输入信息有误,即输入的参数中没有指定计算文件时,退出线程,并输出提示信息,部分代码如下。
if (argc != 2){
printf("input argc error\n");
exit(0);
}
3.4 打不开指定的计算文件
当打不开指定的计算文件时,退出线程,并输出提示信息,部分代码如下。
if ((fd = open(argv[1], O_RDONLY)) < 0){
printf("open the %s file error\n", argv[1]);
exit(0);
}
3.5 堆中申请结构体不成功
当在堆中申请定义结构体的空间不成功时,退出线程,并输出提示信息,部分代码如下。
if ((jp = (job *)malloc(sizeof(job))) == NULL){
printf("%dth thread malloc error\n", num);
exit(0);
}
3.6 请求错误
当程序在运行的过程中,如果请求中有错误请求时则会产生错误报告,不退出继续执行下一个计算:如输入 4 / 0时,因为除数不能为0,所以会产生"4 / 0, b cannot be zero"的报告。并输出提示信息,部分代码如下。
if (jp->b == 0){
printf("%d / %d, b cannot be zero\n", jp->a, jp->b);
jp->answer = -1;
} else {
jp->answer = (double)jp->a / jp->b;
}
四、流程图说明
4.1 主控线程流程图
图2 主控线程流程图
程序开始运行时,首先进行初始化,完成准备工作,定义结构体、队列、变量等,然后创建请求线程,创建计算线程,在睡眠一秒钟的情况下,等待计算结果的输出,计算结果输出后再将计算结果打印输出,程序的主控线程如图所示。
4.2 计算线程流程图
图3 计算线程流程图
4.3 请求线程流程图
图4 请求线程流程图
五、性能分析
5.1 时间复杂度
时间复杂度是指执行算法所需要的计算工作量,它定量描述了该算法的运行时间。
5.2 空间复杂度
空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。
对于存储算法本身所占用的存储空间,从multi_pthread.c这个文件来说,大小为4.31KB,文件很小,multi_pthread.c这个文件运行过程中生成的文件大小也是非常小,mutex.c这个文件只有1.2KB,seg.c这个文件只有480B。
对于算法的输入输出数据所占用的存储空间,算法本身输入的数据为存储在文件中的单词数据;算法的输出数据其中之一为输出到荧屏的提示信息,这样虽然在空间复杂度上作出了一点点牺牲,但是给用户带来的信息量却是很大的,带来了很大的方便;另外的输出数据为输出到文件中的数据,从上面文件大小的分析中可知,数据占用的数据量并不大。
对于算法在运行过程中临时占用的存储空间,程序编译后运行的过程中是一定会占用内存的,程序在运行过程中所占用的临时空间主要为定义变量、结构体、互斥锁进程、并行进程的运行、读写操作、程序本身运行等操作所需要的内存空间,可以看出,在保证程序完整执行的情况下,运行过程并不会占用太大的内存空间。
5.3 文件大小
整个作业的文件占用空间为38K,可见文件很小。
六、运行情况说明
6.1 mutex.c程序运行测试
为了测试互斥量和条件变量的工作情况,本人写了一个mutex.c的程序,具体的源代码在附录B中。该程序的大体设计思路是:将变量a赋初值0,然后创建三个子线程,第二个和第三个线程先睡眠等待条件变量出现,然后第一个线程先将a+1,再激活条件变量以广播的形式通知另外两个线程。第二个和第三个线程分别将a加上各自的线程序号。如果我们的逻辑正确,应该得到a=6,并且a=1应该最先出现,接着是2,3线程的运行结果,否则互斥量失效。下面是运行的结果:
由图运行结果可以看出,程序的执行顺序与我们的逻辑一致,说明互斥量和条件变量起了作用。
6.2 总程序multi_pthread.c的运行测试
对于异常情况做简单的测试,输入0个参数,看程序运行:
输出异常情况报错!
如果将错误的文件输入作为参数,我们将会看到程序报错:
运行程序,指定计算数据的文件为test文件,并进行计算:
可以看出不但完全计算出了test文件中的运算式,而且能进行运算报错,当除数为0时,可以输出错误报告,在结果中用-1代表error。
还能看出表达式的顺序与test文件中的顺序不一样,这是因为各请求线程放入请求队列的顺序不确定,同时计算队列在计算后放入结果队列的顺序也不确定。在test文件比较大的时候更容易看出这种差异。
七、代码附录
A.总程序源码
//use many pthread to read and use manypthread to calculate
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#define MAXLINE 20
#define MAXRTHREAD 3
#define MAXCTHREAD 3
int fd = -1;
typedef struct Queue queue;
typedef struct Job job;
struct Job {
struct Job *next;
int a;
int b;
char str[MAXLINE];
double answer;
};
struct Queue {
struct Job *head;
struct Job *tail;
};
pthread_mutex_t qlock =PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t qlock2 =PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t qready =PTHREAD_COND_INITIALIZER;
struct Queue *qp = NULL;
queue *qp_ans;
/*initialize a queue*/
void queue_init(queue *qp)
{
qp->head = NULL;
qp->tail = NULL;
}
/*append a job on the tail of the queue*/
void job_insert(queue *qp, job *jp)
{
jp->next = NULL;
if (qp->tail != NULL){
qp->tail->next = jp;
}else {
qp->head = jp;
}
qp->tail = jp;
}
void *pthr_req(void *argc)
{
int num = (int)argc;
char str[MAXLINE] = {0};
int a = 0, b = 0;
job *jp;
while (scanf("%d%s%d", &a, str, &b) != EOF){
if ((jp = (job *)malloc(sizeof(job))) == NULL){
printf("%dth thread malloc error\n", num);
exit(0);
}
jp->a = a;
jp->b = b;
strcpy(jp->str, str);
pthread_mutex_lock(&qlock);
job_insert(qp, jp);
pthread_mutex_unlock(&qlock);
pthread_cond_broadcast(&qready);
}
pthread_exit((void *)0);
}
void print_qp(queue *qp)
{
job *jp;
printf("print the required qp\n");
for (jp = qp->head; jp != NULL; jp = jp->next){
printf("%d %s %d\n", jp->a, jp->str, jp->b);
}
}
void print_ans(queue *qp)
{
job *jp;
printf("print the ans qp\n");
for (jp = qp->head; jp != NULL; jp = jp->next){
printf("%d %s %d = %lf\n", jp->a, jp->str, jp->b,jp->answer);
}
}
void job_remove(queue *qp)
{
job *jp;
if (qp->head->next == NULL){
qp->tail = NULL;
}
jp = qp->head->next;
qp->head->next = NULL;
qp->head = jp;
}
void work_qp(queue *sqp)
{
job *jp;
for (jp = sqp->head; jp != NULL; jp = sqp->head){
job_remove(sqp);
if (strcmp(jp->str, "+") == 0){
jp->answer = jp->a + jp->b;
} else if (strcmp(jp->str, "-") == 0) {
jp->answer = jp->a -jp->b;
} else if (strcmp(jp->str, "*") == 0) {
jp->answer = jp->a * jp->b;
} else {
if (jp->b == 0){
printf("%d / %d, b cannotbe zero\n", jp->a, jp->b);
jp->answer = -1;
} else {
jp->answer = (double)jp->a / jp->b;
}
}
pthread_mutex_lock(&qlock2);
job_insert(qp_ans, jp);
pthread_mutex_unlock(&qlock2);
}
}
void *pthr_cal(void *argc)
{
int num = (int)argc;
queue *qp_self;
if ((qp_self = (queue *)malloc(sizeof(queue))) == NULL){
printf("the %dth calculte thread malloc error\n", num);
exit(0);
}
queue_init(qp_self);
job *jp;
while (1){
pthread_mutex_lock(&qlock);
while (qp->head == NULL)
pthread_cond_wait(&qready, &qlock);
for (jp = qp->head; jp != NULL; jp = qp->head){
job_remove(qp);
job_insert(qp_self, jp);
}
pthread_mutex_unlock(&qlock);
work_qp(qp_self);
}
}
int main(int argc, char *argv[])
{
if (argc != 2){
printf("input argc error\n");
exit(0);
}
if ((fd = open(argv[1], O_RDONLY)) < 0){
printf("open the %s file error\n", argv[1]);
exit(0);
}
dup2(fd, STDIN_FILENO);
qp = (queue *)malloc(sizeof(queue));
qp_ans = (queue *)malloc(sizeof(queue));
queue_init(qp);
queue_init(qp_ans);
pthread_t tid = -1;
for (int i = 1; i <= MAXRTHREAD; i++){
if (pthread_create(&tid, NULL, pthr_req, (void *)i) != 0){
printf("create the %dth require thread error\n", i);
exit(0);
}
}
for (int i = 1; i <= MAXCTHREAD; i++){
if (pthread_create(&tid, NULL, pthr_cal, (void *)i) != 0){
printf("create the %dth calculate thread error\n", i);
exit(0);
}
}
sleep(1);
print_ans(qp_ans);
exit(0);
}
B.mutex.c代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
pthread_mutex_t tt =PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t ready =PTHREAD_COND_INITIALIZER;
int a = 0;
void *pthr1(void *argc)
{
pthread_mutex_lock(&tt);
a++;
printf("1th a = %d\n", a);
pthread_mutex_unlock(&tt);
pthread_cond_broadcast(&ready);
}
void *pth2(void *argc)
{
int num = ((int)argc);
pthread_mutex_lock(&tt);
printf("%dth is waiting\n", num);
pthread_cond_wait(&ready, &tt);
a+= num;
printf("%dth a = %d\n", num, a);
pthread_mutex_unlock(&tt);
}
int main()
{
pthread_t tid1, tid2;
if (pthread_create(&tid2, NULL, pth2, (void *)2) != 0){
printf("create pthread error\n");
exit(0);
}
if (pthread_create(&tid2, NULL, pth2, (void *)3) != 0){
printf("create pthread error\n");
exit(0);
}
sleep(1);
if (pthread_create(&tid1, NULL, pthr1, NULL) != 0){
printf("create pthread error\n");
exit(0);
}
sleep(2);
exit(0);
}