目录
多线程编临界资源访问
互斥锁 API 简述
初始化互斥量
互斥量加锁/解锁
互斥量加锁(非阻塞方式)
互斥量销毁
程序示例
多线程编执行顺序控制
信号量 API 简述
初始化信号量
信号量 P/V 操作
信号量申请(非阻塞方式)
信号量销毁
程序示例
条件变量
创建和销毁条件变量
等待条件变量
通知条件变量
程序示例
总结
线程使用流程图
互斥量使用流程图
信号量使用流程图
当线程在运行过程中,去操作公共资源,如全局变量的时候,可能会发生彼 此“矛盾”现象。
例如线程 1 企图想让变量自增,而线程 2 企图想要变量自减, 两个线程存在互相竞争的关系导致变量永远处于一个“平衡状态”,两个线程互相竞争,线程 1 得到执行权后将变量自加,当线程 2 得到执行权后将变量自减, 变量似乎永远在某个范围内浮动,无法到达期望数值
如例程 9 所示
测试例程 9:(Phtread_txex9.c)
#define _GNU_SOURCE
#include
#include
#include
#include
int Num = 0;
void *fun1(void *arg)
{
while(Num < 3){
Num++;
printf("%s:Num = %d\n",__FUNCTION__,Num);
sleep(1);
}
pthread_exit(NULL);
}
void *fun2(void *arg)
{
while(Num > -3){
Num--;
printf("%s:Num = %d\n",__FUNCTION__,Num);
sleep(1);
}
pthread_exit(NULL);
}
int main()
{
int ret;
pthread_t tid1,tid2;
ret = pthread_create(&tid1,NULL,fun1,NULL);
if(ret != 0){
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid2,NULL,fun2,NULL);
if(ret != 0){
perror("pthread_create");
return -1;
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
return 0;
}
为了解决上述对临界资源的竞争问题,pthread 线程引出了互斥锁来解决临界资源访问。通过对临界资源加锁来保护资源只被单个线程操作,待操作结束后解锁,其余线程才可获得操作权。
多个线程都要访问某个临界资源,比如某个全局变量时,需要互斥地访问: 我访问时,你不能访问。
可以使用以下函数进行互斥操作。
函数原型如下:
int pthread_mutex_init(phtread_mutex_t *mutex, const pthread_mutexattr_t *restrict attr);
该函数初始化一个互斥量,第一个参数是改互斥量指针,第二个参数为控制互斥量的属性,一般为 NULL。当函数成功后会返回 0,代表初始化互斥量成功。
当然初始化互斥量也可以调用宏来快速初始化,代码如下:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;
函数原型如下:
互斥量加锁(阻塞)/解锁
#include
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
成功:返回 0
lock 函数与 unlock 函数分别为加锁解锁函数,只需要传入已经初始化好的 pthread_mutex_t 互斥量指针。成功后会返回 0。
当某一个线程获得了执行权后,执行 lock 函数,一旦加锁成功后,其余线 程遇到 lock 函数时候会发生阻塞,直至获取资源的线程执行 unlock 函数后。 unlock 函数会唤醒其他正在等待互斥量的线程。
特别注意的是,当获取 lock 之后,必须在逻辑处理结束后执行 unlock,否则会发生死锁现象!导致其余线程一直处于阻塞状态,无法执行下去。在使用互 斥量的时候,尤其要注意使用 pthread_cancel 函数,防止发生死锁现象!
函数原型如下:
互斥量加锁(非阻塞)
#include
int pthread_mutex_trylock(pthread_mutex_t *mutex);
该函数同样也是一个线程加锁函数,但该函数是非阻塞模式通过返回值来 判断是否加锁成功,用法与上述阻塞加锁函数一致。
函数原型如下:
互斥量销毁
#include
int pthread_mutex_destory(pthread_mutex_t *mutex);
成功:返回 0
该函数是用于销毁互斥量的,传入互斥量的指针,就可以完成互斥量的销毁,成功返回 0。
测试例程 10:(Phtread_txex10.c)
#define _GNU_SOURCE
#include
#include
#include
#include
pthread_mutex_t mutex; //互斥量变量 一般申请全局变量
int Num = 0; //公共临界变量
void *fun1(void *arg)
{
pthread_mutex_lock(&mutex); //加锁 若有线程获得锁,则会阻塞
while(Num < 3){
Num++;
printf("%s:Num = %d\n",__FUNCTION__,Num);
sleep(1);
}
pthread_mutex_unlock(&mutex); //解锁
pthread_exit(NULL); //线程退出 pthread_join 会回收资源
}
void *fun2(void *arg)
{
pthread_mutex_lock(&mutex); //加锁 若有线程获得锁,则会阻塞
while(Num > -3){
Num--;
printf("%s:Num = %d\n",__FUNCTION__,Num);
sleep(1);
}
pthread_mutex_unlock(&mutex); //解锁
pthread_exit(NULL); //线程退出 pthread_join 会回收资源
}
int main()
{
int ret;
pthread_t tid1,tid2;
ret = pthread_mutex_init(&mutex,NULL); //初始化互斥量
if(ret != 0){
perror("pthread_mutex_init");
return -1;
}
ret = pthread_create(&tid1,NULL,fun1,NULL); //创建线程 1
if(ret != 0){
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid2,NULL,fun2,NULL); //创建线程 2
if(ret != 0){
perror("pthread_create");
return -1;
}
pthread_join(tid1,NULL); //阻塞回收线程 1
pthread_join(tid2,NULL); //阻塞回收线程 2
pthread_mutex_destroy(&mutex); //销毁互斥量
return 0;
}
上述例程通过加入互斥量,保证了临界变量某一时刻只被某一线程控制, 实现了临界资源的控制。需要说明的是,线程加锁在循环内与循环外的情况。
本历程在进入 while 循环前进行了加锁操作,在循环结束后进行的解锁操作, 如果将加锁解锁全部放入 while 循环内,作为单核的机器,执行结果无异,当有多核机器执行代码时,可能会发生“抢锁”现象,这取决于操作系统底层的实现。
解决了临界资源的访问,但似乎对线程的执行顺序无法得到控制,因线程都是无序执行,之前采用 sleep 强行延时的方法勉强可以控制执行顺序,但此方法在实际项目情况往往是不可取的,其仅仅可解决线程创建的顺序,当创建之后执行的顺序又不会受到控制,于是便引入了信号量的概念,解决线程执行顺序。
测试例程 11:(Phtread_txex11.c)
#define _GNU_SOURCE
#include
#include
#include
#include
void *fun1(void *arg)
{
printf("%s:Pthread Come!\n",__FUNCTION__);
pthread_exit(NULL);
}
void *fun2(void *arg)
{
printf("%s:Pthread Come!\n",__FUNCTION__);
pthread_exit(NULL);
}
void *fun3(void *arg)
{
printf("%s:Pthread Come!\n",__FUNCTION__);
pthread_exit(NULL);
}
int main()
{
int ret;
pthread_t tid1,tid2,tid3;
ret = pthread_create(&tid1,NULL,fun1,NULL);
if(ret != 0){
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid2,NULL,fun2,NULL);
if(ret != 0){
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid3,NULL,fun3,NULL);
if(ret != 0){
perror("pthread_create");
return -1;
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
return 0;
}
运行结果:通过上述例程可以发现,多次执行该函数其次序是无序的,线程之间的竞 争无法控制,通过使用信号量来使得线程顺序为可控的。
注意:信号量跟互斥量不一样,互斥量用来防止多个线程同时访问某个临界资源。信号量起通知作用,线程 A 在等待某件事,线程 B 完成了这件事后就 可以给线程 A 发信号。
函数原型如下:
int sem_init(sem_t *sem,int pshared,unsigned int value);
函数原型如下:
#include
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
成功:返回 0
通过以上 2 个函数可以完成所谓的 PV 操作,即信号量的申请与释放,完成 对线程执行顺序的控制。
函数原型如下:
#include
int sem_trywait(sem_t *sem);
成功:返回 0
此函数是信号量申请资源的非阻塞函数,功能与 sem_wait 一致,唯一区别在于此函数为非阻塞。
函数原型如下:
#include
int sem_destory(sem_t *sem);
成功:返回 0
该函数为信号量销毁函数,执行过后可将信号量进行销毁
测试例程 12:(Phtread_txex12.c)
#define _GNU_SOURCE
#include
#include
#include
#include
#include
sem_t sem1,sem2,sem3;//申请的三个信号量变量
void *fun1(void *arg)
{
sem_wait(&sem1);//因sem1本身有资源,所以不被阻塞 获取后sem1-1 下次会会阻塞
printf("%s:Pthread Come!\n",__FUNCTION__);
sem_post(&sem2);// 使得sem2获取到资源
pthread_exit(NULL);
}
void *fun2(void *arg)
{
sem_wait(&sem2);//因sem2在初始化时无资源会被阻塞,直至14行代码执行 不被阻塞 sem2-1 下次会阻塞
printf("%s:Pthread Come!\n",__FUNCTION__);
sem_post(&sem3);// 使得sem3获取到资源
pthread_exit(NULL);
}
void *fun3(void *arg)
{
sem_wait(&sem3);//因sem3在初始化时无资源会被阻塞,直至22行代码执行 不被阻塞 sem3-1 下次会阻塞
printf("%s:Pthread Come!\n",__FUNCTION__);
sem_post(&sem1);// 使得sem1获取到资源
pthread_exit(NULL);
}
int main()
{
int ret;
pthread_t tid1,tid2,tid3;
ret = sem_init(&sem1,0,1); //初始化信号量1 并且赋予其资源
if(ret < 0){
perror("sem_init");
return -1;
}
ret = sem_init(&sem2,0,0); //初始化信号量2 让其阻塞
if(ret < 0){
perror("sem_init");
return -1;
}
ret = sem_init(&sem3,0,0); //初始化信号3 让其阻塞
if(ret < 0){
perror("sem_init");
return -1;
}
ret = pthread_create(&tid1,NULL,fun1,NULL);//创建线程1
if(ret != 0){
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid2,NULL,fun2,NULL);//创建线程2
if(ret != 0){
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid3,NULL,fun3,NULL);//创建线程3
if(ret != 0){
perror("pthread_create");
return -1;
}
/*回收线程资源*/
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_join(tid3,NULL);
/*销毁信号量*/
sem_destroy(&sem1);
sem_destroy(&sem2);
sem_destroy(&sem3);
return 0;
}
该例程加入了信号量,使得线程的执行顺序变为可控的。在初始化信号量时, 将信号量 1 填入资源,第一个线程调用 sem_wait 函数可以成功获得信号量,在 执行完逻辑后使用 sem_pos 函数来释放。当执行函数 sem_wait 后,会执行 sem 自减操作,使下一次竞争被阻塞,直至通过 sem_pos 被释放
上述例程因 38 行初始化信号量 1 时候,使其默认获取到资源;
第 43、48 行 初始化信号量 2、3 时候,使之没有资源。于是在线程处理函数中,每个线程通过 sem_wait 函数来等待资源,发生阻塞。因信号量 1 初始值为有资源,故可以 先执行线程 1 的逻辑。待执行完第 12 行 sem_wait 函数,会导致 sem1-1,使得 下一次此线程会被阻塞。继而执行至 14 行,通过 sem_post 函数使 sem2 信号量 获取资源,从而冲破阻塞执行线程 2 的逻辑...以此类推完成线程的有序控制。
条件变量时一种同步机制,用来通知其他线程条件满足了。一般是用来通知对方共享数据的状态信息,因此条件变量是结合互斥量来使用的。
函数原型如下:
#include
// 初始化条件变量 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);//cond_attr 通常为 NULL
// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
这些函数成功时都返回 0
函数原型如下:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
这需要结合互斥量一起使用,示例代码如下:
pthread_mutex_lock(&g_tMutex);
// 如果条件不满足则,会 unlock g_tMutex
// 条件满足后被唤醒,会 lock g_tMutex pthread_cond_wait(&g_tConVar, &g_tMutex);
/* 操作临界资源 */
pthread_mutex_unlock(&g_tMutex);
函数原型如下:
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_signal 函数只会唤醒一个等待 cond 条件变量的线程,示例代码如下:
pthread_cond_signal(&g_tConVar);
有关多线程的创建流程如图 所示,首先需要创建线程,一旦线程创 建完成后,线程与线程之间会发生竞争执行,抢占时间片来执行线程逻辑。在 创建线程时候,可以通过创建线程的第四个参数传入参数,在线程退出时亦可 传出参数被线程回收函数所回收,获取到传出的参数。
当多个线程出现后,会遇到同时操作临界公共资源的问题,当线程操作公 共资源时需要对线程进行保护加锁,防止其与线程在此线程更改变量时同时更 改变量,待逻辑执行完毕后再次解锁,使其余线程再度开始竞争。互斥锁创建 流程下图所示。
当多个线程出现后,同时会遇到无序执行的问题。有时候需要对线程的执行顺序做出限定,变引入了信号量,通过 PV 操作来控制线程的执行顺序,如下图所示