印象笔记:进程 第三天 (线程&线程间通信)
一、线程简介
1、线程的基本概念
通过之前的学习我们知道,进程是系统中资源执行和资源分配的最小单位。每个进程都有自己独立的数据区、代码区、堆栈区等,这就造成了当进程切换时,操作系统需要额外的操作来清空旧区域、分配新区域,进程在进行切换时的系统开销比较大。
为了提高效率,绝大多数操作系统都提供了一种“轻量化进程”的概念,这就是线程的概念。
线程(thread),也被称为轻量级进程,是进程内一个相对独立的、可被进程调度的执行单元。若在单个程序中同时运行多个线程完成工作,则称为多线程编程。
2、线程与进程的区别
一个进程可以拥有多个线程,每个线程共享该进程内的系统资源。由于线程共享进程的内存空间,因此任何线程对内存内数据的操作都可能对其他线程产生影响,因此多线程的同步与互斥机制是十分重要的。
线程本身只占用少量的系统资源,其内存空间也只拥有堆栈区与线程控制块(Thread Control Block,简称TCB),因此对线程的调度需要的系统开销会小得多,能够更高效地提高任务的并发度。
简单总结,进程与线程的区别主要在以下3点:
1)地址空间与系统资源:进程间的地址空间与系统资源互相独立,互不干扰;同一进程内各线程共享地址空间与系统资源。一个进程内的线程对其他进程是不可见(私有)的。
2)通信手段:由于进程间互相独立,因此进程间通信必须借助某些手段。进程间通信手段主要有管道、信号、共享内存、SystemV等;而线程共享进程的资源与空间,因此同一个进程的线程间可以直接读写进程的数据段(例如全局变量等)进行通信,不过需要使用同步与互斥机制保证数据一致性。
3)调度与切换:进程占用系统资源较多,因此切换进程时开销较大;而线程占用系统资源较小,因此切换进程时开销较小。
3、线程的资源
一个进程中的多个线程共享以下资源:
执行的命令
静态数据(例如全局变量等)
打开文件的文件描述符
信号处理函数
当前工作目录
用户ID(UID)
用户组ID(GID)
每个线程私有的资源有:
线程标识符(简称线程号,TID)
程序计数器(PC)与相关寄存器
堆栈区(局部变量、函数返回地址等)
错误号errno
信号掩码与优先级
执行状态与属性
对于线程来说,线程编程主要考虑两部分工作:第一部分是线程的创建、控制与删除;第二部分是线程的同步与互斥。二者都可以使用NPTL线程库来实现。
二、线程编程——线程的创建、控制与删除
在Linux系统中,多线程编程是通过第三方的线程库NPTL实现的。
/**********NPTL简介******************/
本地POSIX线程库(New POSIX Thread Library,简称NPTL)是早期Linux系统内Threads模型的改进,它可以让Linux内核高效运行使用POSIX风格编写的线程程序。有测试证明,使用NPTL启动10万个线程大概只需2秒时间,而未使用NPTL则需要15分钟。
NPTL最先发布在RedHat9.0版本中(2003年),老式POSIX线程库的效率太低,因此从这个版本开始,NPTL开始取代老式Linux线程库。
NPTL有以下特性:
采用1:1线程模型
显著提高运行效率
信号处理效率更高
使用NPTL线程库,需要添加头文件#include<pthread.h>,并且在编译时添加线程库-lpthread
/**********NPTL简介end***************/
//在使用线程编程相关函数时,需要额外注意pthread_t类型,该数据类型是线程独有的数据类型,专门用于表示线程标识符,不能使用int类型代替。如果需要输出pthread_t类型数据,使用格式控制符%u(不过可能会出现warning)。
1、创建线程函数pthread_create()
创建线程需要指定线程执行函数,通常使用函数pthread_create()函数来创建一个线程。
线程创建完毕后,就开始执行指定的函数。在该函数执行完毕后,该线程结束。
函数pthread_create()
所需头文件:#include<pthread.h>
函数原型:int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*routine)(void *), void *arg)
函数参数:
thread 创建线程的标识符
attr 线程属性设置(具体见第四章),如设置成NULL则为缺省(默认)(default)属性
routine 线程执行的函数
arg 传递给routine的参数,传给线程执行函数的参数
函数返回值:
成功:0
失败:返回错误码
注意:使用pthread_create()函数时,第一个参数需要指定一个pthread_t类型的变量然后使用地址传递获取线程标识符。
2、线程退出函数pthread_exit()
退出线程需要使用pthread_exit()函数,这个函数属于线程的主动行为。需要注意的是,不能使用exit()函数试图退出线程,因为exit()函数的作用是使当前进程终止,如果某个线程调用了exit()函数,则会使得进程退出,该进程的所有线程都会直接终止。
函数pthread_exit()
所需头文件:#include<pthread.h>
函数原型:void pthread_exit(void *retval)
函数参数:
retval 线程结束时的返回值,可以通过pthread_join()接收
3、等待线程函数pthread_join()
进程与进程之间,父进程使用wait()函数来等待回收子进程,线程内也有类似的机制,使用pthread_join()函数将一直等待到指定的线程结束为止。
函数pthread_join()
所需头文件:#include<pthread.h>
函数原型:int pthread_join(pthread_t thread, void **thread_result)
函数参数:
thread 等待线程的标识符
thread_result 用户定义的指针,当不为NULL时用来接收等待线程结束时的返回值,即pthread_exit()函数内的retval值
函数返回值:
成功:0
失败:返回错误码
4、取消线程函数pthread_cancel()
前面提到,我们可以使用pthread_exit()函数使得线程主动结束。实际应用中,我们经常需要让一个线程去结束另一个线程,此时可以使用pthread_cancel()函数来实现这样的功能。当然,被取消的线程内部需要事先设置取消状态,
可以使用pthread_setcancel()函数或pthread_setcanceltype()函数 来设置线程被取消的状态。
函数pthread_cancel()
所需头文件:#include<pthread.h>
函数原型:int pthread_cancel(pthread_t thread)
函数参数:
thread 需要取消的线程的标识符
函数返回值:
成功:0
失败:返回错误码
被取消的线程可以(使用pthread_setcancel()函数或pthread_setcanceltype()函数)设置自己的取消状态:
-被取消线程接收到另一个线程的取消请求后,是接受还是忽略?
-如果接受,是立即结束操作还是等待某个函数调用?
/**********其他线程操作函数************/
1、获取当前线程标识符函数pthread_self()
函数pthread_self()
所需头文件:#include<pthread.h>
函数原型:pthread_t pthread_self(void)
函数参数:无
函数返回值:当前线程的线程标识符
注意:该函数返回当前线程的线程标识符,即创建线程时pthread_create()函数参数1的值。线程标识符只有在所属的进程内有效。线程标识符在整个系统内是唯一的。
2、比较两个线程的线程标识符是否相等pthread_equal()
函数pthread_equal()
所需头文件:#include<pthread.h>
函数原型:int pthread_equal(pthread_t t1, pthread_t t2)
函数参数:需要比较的两个线程标识符
函数返回值:
非0 相等
0 不相等
注意:线程标识符使用特殊的pthread_t类型,通常情况不能直接像整数一样比较,需要使用pthread_equal()函数才行。该函数主要用于内核移植判定两个内核的线程是否相同。
/**********其他线程操作函数end*********/
示例1:使用pthread_create()函数与pthread_exit()函数,创建线程,并让线程执行指定的函数
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
pthread_t tid; //定义线程标识符,需要定义成全局变量否则子函数无法访问
void *thrd_function(void *arg) //线程需要执行的函数,注意函数定义的写法
{
printf("New Process: PID:%d, TID:%u.\n",getpid(),tid);
pthread_exit(NULL); //退出线程
}
int main()
{
if(pthread_create(&tid,NULL,thrd_function,NULL)!=0)
//第二个参数,缺省设置,第四个参数表示传给第三个参数(要执行函数)的参数,创建成功:返回值:0;失败返回错误码
//注意调用pthread_create()函数的方法,以及第一个参数的写法
//第三个参数也可以写成&thrd_function,但注意不要写成thrd_function()
{
printf("Create thread error!\n");
exit(0);
}
printf("Main Process: PID:%d, TID in pthread_create function %u.\n",getpid(),tid);
sleep(1);
return 0;
}
示例2:创建多个线程,每个线程执行不同的函数
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
pthread_t tid1,tid2,tid3;
void *thrd_function1(void *arg)//线程1需要执行的函数:什么也不做
{
printf("This is 1st thread:\n");
printf("1st TID:%u.\n",tid1);
printf("1st thread will exit\n");
pthread_exit(NULL);//退出线程
}
void *thrd_function2(void *arg)//线程2需要执行的函数:打印传递的字符串
{
printf("This is 2nd thread:\n");
printf("2nd thread will print string:%s\n",(char*)arg);
printf("2nd thread will exit\n");
pthread_exit(NULL);
}
void *thrd_function3(void *arg)//线程3需要执行的函数:计算1+2+3+……+100
{
printf("This is 3rd thread:\n");
printf("3rd thread will calculate:1+2+3+……+100\n");
int i,sum;
for(i=0,sum=0;i<=100;i++)
{
sum+=i;
}
printf("sum is %d\n",sum);
printf("3rd thread will exit\n");
pthread_exit(NULL);
}
int main()
{
if(pthread_create(&tid1,NULL,thrd_function1,NULL)!=0)
{
printf("Create thread1 error!\n");
exit(0);
}
if(pthread_create(&tid2,NULL,thrd_function2,"helloworld")!=0)
{
printf("Create thread2 error!\n");
exit(0);
}
if(pthread_create(&tid3,NULL,thrd_function3,NULL)!=0)
{
printf("Create thread3 error!\n");
exit(0);
}
sleep(1);
return 0;
}
由示例2的程序我们可以看到,3个线程的执行完全是随机的,无法事先预制线程运行的顺序。
示例3:使用pthread_join()函数调整线程的运行顺序,让线程2先执行,线程1等待线程2退出后(即让线程1回收线程2的资源后)执行,线程3等待线程1退出后(即让线程3回收线程1的资源后)执行,主函数进程等待线程3退出后执行
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
pthread_t tid1,tid2,tid3;
void *tret;
void *thrd_function1(void *arg)//线程1需要执行的函数
{
if(pthread_join(tid2,&tret)!=0)//等待线程2结束,线程2结束的返回值存放在tret中
{
printf("Join thread 2 error\n");
exit(0);
}
printf("Thread 2 exit code:%d\n",(int)tret);
printf("This is 1st thread:\n");
printf("1st TID:%u.\n",tid1);
printf("1st thread will exit\n");
pthread_exit((void*)1);//退出线程
}
void *thrd_function2(void *arg)//线程2需要执行的函数
{
printf("This is 2nd thread:\n");
printf("2nd thread will print string:%s\n",(char*)arg);
printf("2nd thread will exit\n");
pthread_exit((void*)2);
}
void *thrd_function3(void *arg)//线程3需要执行的函数
{
if(pthread_join(tid1,&tret)!=0)//等待线程1结束,线程1结束的返回值存放在tret中
{
printf("Join thread 1 error\n");
exit(0);
}
printf("Thread 1 exit code:%d\n",(int)tret);
printf("This is 3rd thread:\n");
printf("3rd thread will calculate:1+2+3+……+100\n");
int i,sum;
for(i=0,sum=0;i<=100;i++)
{
sum+=i;
}
printf("sum is %d\n",sum);
printf("3rd thread will exit\n");
pthread_exit((void*)3);
}
int main()
{
if(pthread_create(&tid1,NULL,thrd_function1,NULL)!=0)
{
printf("Create thread1 error!\n");
exit(0);
}
if(pthread_create(&tid2,NULL,thrd_function2,"helloworld")!=0)
{
printf("Create thread2 error!\n");
exit(0);
}
if(pthread_create(&tid3,NULL,thrd_function3,NULL)!=0)
{
printf("Create thread3 error!\n");
exit(0);
}
if(pthread_join(tid3,&tret)!=0)//等待线程3结束,线程3结束的返回值存放在tret中
{
printf("Join thread 3 error\n");
exit(0);
}
printf("Thread 3 exit code:%d\n",(int)tret);
printf("This is Main Process %d\n",getpid());
sleep(1);
return 0;
}
该程序简单地实现了排列几个线程间的执行顺序,但是该方法并不常用。更加常用的方式是采用同步与互斥机制。有关同步与互斥机制我们会在下章讲解。
注意:一个线程只能被其他的唯一线程等待,如果有多个线程join同一个线程则会报错。因此,使用pthread_join()函数等待线程结束时需要注意第一个参数(线程标识符)是否与其他的pthread_join()函数冲突。
示例4:创建3个线程,让3个线程同时执行同一个函数,每个线程执行一个5次循环(看成执行5个小任务)。为了模拟每个任务执行时间与完成时间的随机性,每次执行循环之前都会等待1~6秒的时间。
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<time.h>
#include<math.h>
#define THREAD_NUM 3 /*线程数*/
#define REPEAT_NUM 5 /*每个线程执行的循环次数*/
#define DELAY_TIME 6 /*每次循环的最大间隔*/
void *thrd_function(void *arg)
{
int thrd_num = (int)arg; //线程号,arg为void所以要强转为int型
int delay_time = 0; //延迟时间
int count = 0; //用来for循环的变量,线程的执行次数
printf("Thread %d is running!\n",thrd_num);
for(count=0;count<REPEAT_NUM;count++) //开始执行线程6次
{
delay_time = rand()%DELAY_TIME+1;//随机生成1~6,代表当次的等待时间,主函数中有srand(time(NULL))
sleep(delay_time);//延时
printf("\tThread %d: job %d delay=%d\n",thrd_num,count,delay_time);//显示:线程号、执行的第几次、延时的时间
}
printf("Thread %d finished\n",thrd_num); //表示该进程已经循环执行结束
pthread_exit(NULL);
}
int main()
{
pthread_t thread[THREAD_NUM]; //线程数组,3个
int no = 0; //用于循环创建3个线程
void *thrd_ret; //用于接受每次线程结束时返回给主函数的值
srand(time(NULL)); //随机延时用
for(no=0;no<THREAD_NUM;no++) //循环创建线程
{
if(pthread_create(&thread[no],NULL,thrd_function,(void*)no)!=0) //创建多线程
{
printf("Create thread %d error!\n",no); //创建失败,会显示第几个线程创建失败
exit(0);
}
}
printf("Create all threads success, Waiting threads to finish……\n");
for(no=0;no<THREAD_NUM;no++)//设定循环,主函数进程等待所有线程结束
{
if(pthread_join(thread[no],&thrd_ret)!=0)//等待线程结束,thrd_ret 用于接受线程退出时返回的值
{
printf("Join thread %d error\n",no);
exit(0);
}
else
{
printf("Thread %d has been joined by MainProcess\n",no);//表示主函数进程在等待该线程结束
}
}
return 0;
}
三、线程编程——线程的同步与互斥
与进程需要专用的通信手段不同,线程与线程之间由于共享进程的资源与地址空间,因此线程间可直接通信(类似函数与函数间使用全局变量传递数据)。但是由于多个线程共同使用资源与地址空间,因此必须考虑线程间资源访问的同步与互斥。
NPTL线程库有两种常用的线程间同步与互斥的机制:互斥锁与信号量。互斥锁比较适合同时可用的资源是唯一的情况,而信号量比较适合同时可用的资源为多个的情况。
1、线程控制——互斥锁mutex
互斥锁通过简单的加锁方法来保证对共享资源的原子操作。互斥锁只有两种状态:上锁与解锁。在同一时刻只能有一个线程使用互斥锁,拥有互斥锁的线程可以对线程的共享资源进行访问直至解锁;其他未获得互斥锁的线程在此期间必须等待直至有线程解锁为止。
互斥锁函数都是含有"mutex"字样的函数。
/*************原子操作*************/
原子操作(atomic operation)指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何线程切换动作。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。将整个操作视作一个整体是原子性的核心特征。
/*************原子操作end**********/
互斥锁机制有5个函数:
pthread_mutex_init():初始化互斥锁
pthread_mutex_lock():互斥锁上锁(阻塞)
pthread_mutex_trylock():互斥锁判断上锁(非阻塞)
pthread_mutex_unlock():互斥锁解锁
pthread_mutex_destroy():删除互斥锁
以下是这些函数的函数原型:
函数pthread_mutex_init()
所需头文件:#include<pthread.h>
函数原型:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
函数参数:
mutex 互斥锁(指针)
mutexattr 互斥锁的属性,如设置成NULL则为缺省(default)属性(指针)
函数返回值:
成功:0
失败:返回错误码
通常情况下我们会定义一个pthread_mutex_t类型的全局变量来表示互斥锁。
//其余4个函数的参数列表与返回值相同,放在一起讲解
函数:
pthread_mutex_lock()
pthread_mutex_trylock()
pthread_mutex_unlock()
pthread_mutex_destroy()
所需头文件:#include<pthread.h>
函数原型:
int pthread_mutex_lock()(pthread_mutex_t *mutex) //上锁,若不成功则等待
int pthread_mutex_trylock()(pthread_mutex_t *mutex) //上锁,若不成功则返回失败
int pthread_mutex_unlock()(pthread_mutex_t *mutex) //解锁
int pthread_mutex_destroy()(pthread_mutex_t *mutex) //删除互斥锁
函数参数:
mutex 互斥锁
函数返回值:
成功:0
失败:-1
注意:使用5个mutex函数时,第一个参数需要指定一个pthread_mutex_t类型的变量,然后使用地址传递获取mutex变量。
示例:该示例是在上一章的示例4的基础上,添加了mutex相关函数,使得原本无序运行的程序变得按一定顺序运行。注意添加的互斥锁相关的代码
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<time.h>
#include<math.h>
#define THREAD_NUM 3 /*线程数*/
#define REPEAT_NUM 5 /*每个线程执行的循环次数*/
#define DELAY_TIME 6 /*每次循环的最大间隔*/
pthread_mutex_t mutex; //互斥锁
void *thrd_function(void *arg)
{
int thrd_num = (int)arg;
int delay_time = 0;
int count = 0;
if(pthread_mutex_lock(&mutex)) //地址传递,用于获取mutex的值;上锁成功会返回0,所以成功就不会执行下面的报错语句了
{
printf("Thread %d lock failed\n",thrd_num);
pthread_exit(NULL);//如果上锁失败则直接结束线程
}
printf("Thread %d is running!\n",thrd_num);
for(count=0;count<REPEAT_NUM;count++)
{
delay_time = rand()%DELAY_TIME+1;//随机生成1~6,代表当次的等待时间
sleep(delay_time);//延时
printf("\tThread %d: job %d delay=%d\n",thrd_num,count,delay_time);
}
printf("Thread %d finished\n",thrd_num);
pthread_mutex_unlock(&mutex);//解锁,让其余线程运行
pthread_exit(NULL);
}
int main()
{
pthread_t thread[THREAD_NUM];
int no = 0;
void *thrd_ret;
srand(time(NULL));
pthread_mutex_init(&mutex,NULL);//互斥锁初始化,否则无法使用,初始化成功:返回0;这里也可以加一个判断,判断是否初始化成功
for(no=0;no<THREAD_NUM;no++)
{
if(pthread_create(&thread[no],NULL,thrd_function,(void*)no)!=0) //创建多线程
{
printf("Create thread %d error!\n",no);
exit(0);
}
}
printf("Create all threads success, Waiting threads to finish……\n");
for(no=0;no<THREAD_NUM;no++)//设定循环,主函数进程等待所有线程结束
{
if(pthread_join(thread[no],&thrd_ret)!=0)//等待线程结束
{
printf("Join thread %d error\n",no);
exit(0);
}
else
{
printf("Thread %d has been joined by MainProcess\n",no);//表示主函数进程在等待该线程结束
}
}
pthread_mutex_destroy(&mutex);//运行结束,删除互斥锁
return 0;
}
可以发现,在使用了互斥锁之后,该程序的运行不会同以前一样杂乱无章,而是按一定的顺序执行原子操作。
2、线程控制——信号量semaphore
使用互斥锁可以实现两个线程之间的互斥操作,但是无法真正实现线程间的同步操作。此时我们可以使用信号量控制线程。信号量的函数都是含有"sem"字样的函数。
信号量是最早出现的用来解决进程同步与互斥问题的机制,包括一个称为信号量的变量及对它进行的两个原语操作(PV操作)。
/***************PV操作******************/
1962年,Dijkstra教授来到荷兰南部的艾恩德霍芬技术大学(Eindhoven Technical University)任数学教授。在这里,他参加了X86计算机的开发。针对操作系统需要同步的问题,他提出了一种称为“PV操作”的同步机制。
Dijkstra教授巧妙地利用火车运行控制系统中的“信号灯”(semaphore,或叫“信号量”)概念加以解决。例如有进程P1与P2,为了防止这两个进程并发时产生错误,狄克斯特拉设计了一种同步机制叫“PV操作”,P操作和V操作是执行时不被打断的两个操作系统原语。
P操作(通过):对信号量减1,若结果大于等于0,则进程继续,否则(小于0)执行P操作的进程被阻塞等待释放
V操作(释放):对信号量加1,若结果小于等于0,则唤醒队列中一个因为P操作而阻塞的进程,否则不必唤醒进程
//由于Dijkstra教授使用荷兰语,在荷兰语中,“通过”叫“passeren”,“释放”叫“vrijgeven”,PV操作因此得名,这是极少数的在计算机科学中不使用英语表达的例子之一
一般情况下,对于同一信号量,先执行P操作,然后执行V操作。PV操作成对出现。
注意:P、V原语必须成对使用,否则可能会出现死循环。如果出现多个分支,需要认真检查PV操作是否成对。
下面我们具体来看几个PV操作的模型。
示例1:生产者——消费者问题(单缓冲区模型)
这是一个非常有名的模型。生产者负责生产产品,生产后送入临界区(即生产者与消费者之间存放产品的缓冲区),消费者负责消费产品,需要从临界区内取出产品后进行消费。临界区大小限定为1(即同一时刻只能存放最多1个产品)
很明显,当临界区有产品时,生产者不能将产品送入临界区;当临界区无产品时,消费者也不能从临界区拿走产品从而进行消费。生产者与消费者两个进程虽然是异步的,但是必须保持同步才能正常运行。
单缓冲区模型的生产者——消费者问题可以使用以下的PV操作实现
1.设定两个信号量s1和s2,s1初值为1,s2初值为0
2.生产者的流程如下:
生产一个产品
P(S1)
将产品送入临界区
V(S2)
3.消费者的流程如下:
P(S2)
从临界区拿一个产品
V(S1)
消费产品
示例2:公交车司机与公交车售票员问题
这是一个非常有名的模型。一辆公交车需要两个工作人员协作:司机负责开车,售票员负责开关车门以及售票。在售票员未关闭车门的前提下司机不能开车,同样,车辆未到站司机未停车之前售票员也无法开车门。
公交车司机与公交车售票员问题可以使用以下的PV操作实现
1.设定两个信号量s1和s2,s1初值为0,s2初值为0
2.司机的流程如下:
P(S1)
启动车辆
正常行驶
到站停车
V(S2)
3.售票员的流程如下:
关车门
V(S1)
售票
P(S2)
开车门
上下乘客
/***************PV操作end***************/
使用信号量相关函数可以实现同步与互斥的相关操作:若需要实现线程互斥操作,则需要一个信号量;若需要实现线程同步操作,则需要两个信号量。
使用信号量控制两个线程互斥:
1.初始化一个信号量sem,初值为1
2.线程1流程:
P(sem)
线程1执行
V(sem)
3.线程2流程:
P(sem)
线程2执行
V(sem)
通过一个信号量控制两个线程互斥,当线程1在运行时,线程2就无法运行;同样,线程2在运行时,线程1就无法运行。
使用信号量控制两个线程同步:
1.初始化两个信号量sem1和sem2,sem1初值为1,sem2初值为0
2.线程1流程:
P(sem1)
线程1执行
V(sem2)
3.线程2流程:
P(sem2)
线程2执行
V(sem1)
通过两个信号量控制两个线程同步,两个线程之间使用两个信号量互相制约实现同步
1.信号量:本质是资源
2.信号量大小:资源的个数
3.P操作的本质:消耗资源
4.V操作的本质:生成资源
5.P操作阻塞进程的时机:P操作之后,信号量小于0,代表未得到资源,所以阻塞
6.V操作释放进程的时机:V操作之后,信号量小于等于0,代表正在亏欠被阻塞的进程资源,所以释放
下面我们看看信号量处理函数。Linux内的NPTL线程库内的线程信号量处理函数有很多,这里介绍常用的函数:
sem_init():初始化信号量
sem_wait():获取信号量并对信号量变量减1,相当于P操作。若不成功则会阻塞线程。
sem_trywait():作用同sem_wait(),二者的区别在于sem_wait()会阻塞线程而sem_trywait()会立即返回。
sem_post():将信号量的值加1同时唤醒一个线程,相当于V操作
sem_getvalue():用于获取当前信号量的值
sem_destroy():用于删除信号量
以下是这些函数的函数原型:
函数sem_init()
所需头文件:#include<semaphore.h>
函数原型:int sem_init(sem_t *sem, int pshared, unisgned int value)
函数参数:
sem 信号量对象
pshared 决定信号量是否能在不同进程的线程间共享。默认使用0(不共享)
value 信号量初始化的值
函数返回值:
成功:0
失败:-1
//其余5个函数的参数列表与返回值相同,放在一起讲解
函数:
sem_wait() P
sem_trywait()
sem_post() V
sem_getvalue()
sem_destroy()
所需头文件:#include<semaphore.h>
函数原型:
int sem_wait(sem_t *sem) //信号量减1(P操作),若不成功则等待(阻塞)
int sem_trywait(sem_t *sem) //信号量减1(P操作),若不成功则返回错误,并不会阻塞线程
int sem_post(sem_t *sem) //信号量加1(V操作)并释放一个阻塞的线程
int sem_getvalue(sem_t *sem)//获取信号量的值
int sem_destroy(sem_t *sem) //删除信号量
函数参数:
sem 信号量对象
函数返回值:
成功:0
失败:-1
示例1:演示两个线程同步
主函数进程从键盘读入用户输入的字符串,若字符串是"quit"则程序结束,否则线程计算输入字符串的长度。注意该示例中主函数进程与创建的线程之间的协同运行。注意信号量处理函数的用法。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
char buf[60];
sem_t sem;
void *function(void *arg)
{
printf("This is thread2\n");
while(1)
{
if(sem_wait(&sem)==0) //P操作
{
printf("Thread2 is waiting……\n");
}
printf("You enter %d characters\n", strlen(buf) - 1);
}
}
int main()
{
pthread_t a_thread;
void *thread_result;
if(sem_init(&sem, 0, 0) < 0) //注意第二个参数只能填0,表示不共享信号量;第三个参数,初始化信号量值为:0
{
perror("fail to sem_init");
exit(-1);
}
printf("This is MainProcess\n");
if(pthread_create(&a_thread, NULL, function, NULL) < 0)
{
perror("fail to pthread_create");
exit(-1);
}
printf("input 'quit' to exit\n");
do
{
fgets(buf, 60, stdin);
sem_post(&sem); //V操作;传址操作
}while(strncmp(buf, "quit", 4) != 0); //strncmp,限制字符长度的比较,完全相同时,返回值为:0
sem_destroy(&sem); //删除信号量
sleep(1);
return 0;
}
示例2:编程实现“公交车司机——公交车售票员”问题
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
pthread_t tid1,tid2;
sem_t sem1,sem2;
void *bus_driver(void *arg) //司机线程
{
printf("司机线程开始运行……\n");
while(1)
{
if(sem_wait(&sem1)==0) //P(sem1)
{
printf("司机发现关闭车门\n");
}
printf("司机启动车辆\n");
sleep(1);
printf("司机驾驶车辆\n");
sleep(10); //开车时间稍长方便观察程序运行效果
printf("车辆到站,司机停车\n");
sem_post(&sem2); //V(sem2)
}
}
void *bus_conductor(void *arg) //售票员线程
{
printf("售票员线程开始运行……\n");
while(1)
{
printf("售票员关闭车门\n");
sleep(1); //保证时司机线程先P(sem1)
sem_post(&sem1); //V(sem1)
printf("售票员开始卖票\n");
sleep(5); //卖票时间稍长方便观察程序运行效果
printf("售票员卖票完毕\n");
if(sem_wait(&sem2)==0) //P(sem2)
{
printf("售票员发现车辆到站\n");
}
printf("车辆到站,售票员开车门\n");
printf("售票员让乘客上下车\n");
sleep(1);
}
}
int main(int argc, const char *argv[])
{
if(sem_init(&sem1, 0, 0) < 0) //sem1设置初始值:0
{
perror("fail to sem_init");
exit(-1);
}
if(sem_init(&sem2, 0, 0) < 0) //sem2设置初始值:0
{
perror("fail to sem_init");
exit(-1);
}
if(pthread_create(&tid1,NULL,bus_driver,NULL)!=0) //开始司机线程
{
printf("Create thread1 error!\n");
exit(0);
}
if(pthread_create(&tid2,NULL,bus_conductor,NULL)!=0) //开始售票员线程
{
printf("Create thread2 error!\n");
exit(0);
}
if(pthread_join(tid1,NULL)!=0) //等待司机线程的结束
{
printf("Join thread 1 error\n");
exit(0);
}
if(pthread_join(tid2,NULL)!=0) //等待售票员线程的结束
{
printf("Join thread 2 error\n");
exit(0);
}
sem_destroy(&sem1); //清除信号量sem1
sem_destroy(&sem2); //清除信号量sem2
sleep(1);
return 0;
}
练习:有一家四口:父亲、母亲、儿子、女儿。一天儿子想吃橘子,但是女儿想吃苹果。父亲负责供应苹果,母亲负责供应橘子。但是盛水果的盘子在同一时刻只能放下一个水果。使用线程的互斥与同步模拟一家四口放水果/吃水果的模型
父亲--1个苹果-->盘子--1个苹果-->女儿
或者
母亲--1个橘子-->盘子--1个橘子-->儿子
答案:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
pthread_t tid_dad,tid_mom,tid_son,tid_dau; //四个消费者
sem_t plate,apple,orange; //三种资源
void *dad(void *arg)
{
printf("父亲开始放入苹果……\n");
while(1)
{
sleep(1); //延时1s
if(sem_wait(&plate)==0) //P(plate)
{
printf("父亲放入一个苹果\n");
}
sleep(1);
sem_post(&apple); //V(apple)
}
}
void *mom(void *arg)
{
printf("母亲开始放入橘子……\n");
while(1)
{
sleep(2); //延时2s;这里就让父亲先放苹果了,如果都为1s的话,系统随机分配
if(sem_wait(&plate)==0) //P(plate)
{
printf("母亲放入一个橘子\n");
}
sleep(1);
sem_post(&orange); //V(orange)
}
}
void *son(void *arg)
{
printf("儿子想吃橘子……\n");
while(1)
{
if(sem_wait(&orange)==0) //P(orange)
{
printf("儿子发现盘子内有橘子\n");
}
printf("儿子拿走橘子\n");
sleep(1);
sem_post(&plate); //V(plate)
printf("儿子吃掉橘子\n");
sleep(1);
}
}
void *dau(void *arg)
{
printf("女儿想吃苹果……\n");
while(1)
{
if(sem_wait(&apple)==0) //P(apple)
{
printf("女儿发现盘子内有苹果\n");
}
printf("女儿拿走苹果\n");
sleep(1);
sem_post(&plate); //V(plate)
printf("女儿吃掉苹果\n");
sleep(1);
}
}
int main()
{
if(sem_init(&plate,0,1)<0) //初始化plate为:1
{
perror("fail to sem_init plate");
exit(-1);
}
if(sem_init(&apple,0,0)<0) //初始化apple为:0
{
perror("fail to sem_init apple");
exit(-1);
}
if(sem_init(&orange,0,0)<0) //初始化orange为:0
{
perror("fail to sem_init orange");
exit(-1);
}
if(pthread_create(&tid_dad,NULL,dad,NULL)!=0) //创建线程dad
{
printf("Create thread-dad error!\n");
exit(0);
}
if(pthread_create(&tid_mom,NULL,mom,NULL)!=0) //创建线程mom
{
printf("Create thread-mom error!\n");
exit(0);
}
if(pthread_create(&tid_son,NULL,son,NULL)!=0) //创建线程son
{
printf("Create thread-son error!\n");
exit(0);
}
if(pthread_create(&tid_dau,NULL,dau,NULL)!=0) //创建线程dau
{
printf("Create thread-dau error!\n");
exit(0);
}
if(pthread_join(tid_dad,NULL)!=0) //等待线程dad的结束
{
printf("Join thread-dad error\n");
exit(0);
}
if(pthread_join(tid_mom,NULL)!=0) //等待线程mom的结束
{
printf("Join thread-mom error\n");
exit(0);
}
if(pthread_join(tid_son,NULL)!=0) //等待线程son的结束
{
printf("Join thread-son error\n");
exit(0);
}
if(pthread_join(tid_dau,NULL)!=0) //等待线程dau的结束
{
printf("Join thread-dau error\n");
exit(0);
}
sem_destroy(&plate); //清除信号量
sem_destroy(&apple);
sem_destroy(&orange);
sleep(1);
return 0;
}
思考:若盘子可以同时容纳最多10个水果,且放水果的速度要快于吃水果的速度。那么该程序后会出现什么效果?修改程序验证自己的猜想。
四、线程编程——线程属性(选学)
在讲解pthread_create()函数时,该函数的第二个参数(pthread_attr_t *attr)我们设置成NULL。那么这个参数到底有什么含义呢?
实际上,这个参数表示了线程的属性。线程的属性包括:
-线程的分离状态detachstate
-线程的调度策略schedpolicy
-线程的调度参数schedparam
-线程的继承性inheritsched
-线程的作用域scope
-线程栈末尾的警戒缓冲区大小guardsize
-线程栈设置stackaddr_set
-线程栈位置stackaddr
-线程栈的大小stacksize
默认的线程属性(即参数为NULL的情况)为:非绑定、非分离、缺省1M的堆栈、与父进程同等优先级
线程的属性无法直接设置,必须执行相关的函数进行操作。若想设置线程的属性,则首先需要执行pthread_attr_init()函数,该函数必须在pthread_create()函数之前调用。待线程运行结束后,需要使用pthread_attr_destroy()函数释放线程的资源。
以下是线程设置函数的简介:
1、线程的作用域(scope)
作用域属性描述特定线程将与哪些线程竞争资源。线程可以在两种竞争域内竞争资源:
1.进程域(process scope):与同一进程内的其他线程。
2.系统域(system scope):一个具有系统域的线程将与整个系统中所有具有系统域的线程按照优先级竞争处理器资源,进行调度。
2、线程的绑定状态(binding state)
关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)
轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。
1.非绑定状态:默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。
绑定状态
2.绑定状况:则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。
3、线程的分离状态(detached state)
线程的分离状态决定一个线程以什么样的方式来终止自己。
1.非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
2.分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。
线程分离状态的函数是pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选为 PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。
这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
4、线程的优先级(priority)
1.新线程的优先级为默认为0。
2.新线程不继承父线程调度优先级(PTHREAD_EXPLICIT_SCHED)
3.仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0
5、线程的栈地址(stack address)
POSIX.1定义了两个常量_POSIX_THREAD_ATTR_STACKADDR 和_POSIX_THREAD_ATTR_STACKSIZE检测系统是否支持栈属性。也可以给sysconf()函数传递_SC_THREAD_ATTR_STACKADDR或 _SC_THREAD_ATTR_STACKSIZE来进行检测。
当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstackaddr和pthread_attr_getstackaddr两个函数分别设置和获取线程的栈地址。传给pthread_attr_setstackaddr函数的地址是缓冲区的低地址(不一定是栈的开始地址,栈可能从高地址往低地址增长)。
6、线程的栈大小(stack size)
当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用。
当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
函数pthread_attr_getstacksize和 pthread_attr_setstacksize提供设置。
7、线程的栈保护区大小(stack guard size)
保护区的作用是在线程栈顶留出一段空间,防止栈溢出。当栈指针进入这段保护区时,系统会发出错误,通常是发送信号给线程。
该属性默认值是PAGESIZE大小,该属性被设置时,系统会自动将该属性大小补齐为页大小的整数倍。当改变栈地址属性时,栈保护区大小通常清零。
8、线程的调度策略(schedpolicy)
POSIX标准指定了三种调度策略:先入先出策略 (SCHED_FIFO)、循环策略 (SCHED_RR) 和自定义策略 (SCHED_OTHER)。SCHED_FIFO 是基于队列的调度程序,对于每个优先级都会使用不同的队列。SCHED_RR 与 FIFO 相似,不同的是前者的每个线程都有一个执行时间配额。SCHED_FIFO 和 SCHED_RR 是对 POSIX Realtime 的扩展。SCHED_OTHER 是缺省的调度策略。
1.新线程默认使用 SCHED_OTHER 调度策略。线程一旦开始运行,直到被抢占或者直到线程阻塞或停止为止。
2.SCHED_FIFO:如果调用进程具有有效的用户ID 0,则争用范围为系统 (PTHREAD_SCOPE_SYSTEM) 的先入先出线程属于实时 (RT) 调度类。如果这些线程未被优先级更高的线程抢占,则会继续处理该线程,直到该线程放弃或阻塞为止。对于具有进程争用范围 (PTHREAD_SCOPE_PROCESS)) 的线程或其调用进程没有有效用户 ID 0 的线程,请使用 SCHED_FIFO,SCHED_FIFO 基于 TS 调度类。
3.SCHED_RR:如果调用进程具有有效的用户 ID 0,则争用范围为系统 (PTHREAD_SCOPE_SYSTEM)) 的循环线程属于实时 (RT) 调度类。如果这些线程未被优先级更高的线程抢占,并且这些线程没有放弃或阻塞,则在系统确定的时间段内将一直执行这些线程。对于具有进程争用范围 (PTHREAD_SCOPE_PROCESS) 的线程,请使用 SCHED_RR(基于 TS 调度类)。此外,这些线程的调用进程没有有效的用户 ID 0。
9、线程并行级别(concurrency)
应用程序使用 pthread_setconcurrency() 通知系统其所需的并发级别。