线程的基本概念(三)同步与互斥关系、什么是死锁
程序员文章站
2022-05-22 11:21:39
...
在前面两篇中介绍了线程的基本概念和线程控制
今天来看一下线程之间的同步和互斥关系
互斥关系
线程之间的互斥关系
对于一块临界资源,同一时间只能有一个线程进行访问,对于之前学习的进程间通信中讲的管道和消息队列,均内置的互斥同步机制。 |
大部分情况下,线程使用的函数都是全局的,如果这样的话,就可能发生当一个线程正在访问一资源时,另外一个线程也来访问该资源,此时就可能发生逻辑错误。经典场景即使售票机制。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
//************购票问题
int ticket=10;
pthread_mutex_t mutex;
void * Entry(void * arg)
{
(void)arg;
while(1)
{
if(ticket>0)//如果还有票。就执行买票,票数减一
{
ticket--;
printf("ticket:%d\n",ticket);
}
else//没有票就退出
{
break;
}
sleep(1);
}
return NULL;
}
void test()
{
pthread_t thread[10];
//创建线程
int i=0;
for(i=0;i<10;i++)
{
pthread_create(&thread[i],NULL,Entry,NULL);
}
//进行线程等待
for(i=0;i<10;i++)
{
pthread_join(thread[i],NULL);
}
}
int main()
{
test();
return 0;
}
利用互斥量来解决上面的问题,保证每次判断票数不为0 和票数减一这两个操作为原子操作。即对访问临临界资源的那段代码进行上锁。
mutex 互斥量
基于cpu中实现了将寄存器中的值和内存中的值交换的原子操作
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
//进程间互斥关系
//购票问题
int ticket=10;
pthread_mutex_t mutex;
void * Entry(void * arg)
{
(void)arg;
while(1)
{
//获取互斥锁,保证下面的操作一次全部执行完(原子操作)
pthread_mutex_lock(&mutex);
if(ticket>0)
{
ticket--;
printf("ticket:%d\n",ticket);
}
else
{
break;
}
pthread_mutex_unlock(&mutex);
//释放互斥锁,保证其他线程可以进行访问
sleep(1);
}
return NULL;
}
void test()
{
pthread_t thread_1,thread_2;
//初始化互斥量
pthread_mutex_init(&mutex,NULL);
//创建两个线程
pthread_create(&thread_1,NULL,Entry,NULL);
pthread_create(&thread_2,NULL,Entry,NULL);
//进行线程等待
pthread_join(thread_1,NULL);
pthread_join(thread_2,NULL);
//销毁互斥量
pthread_mutex_destroy(&mutex);
}
int main()
{
test();
return 0;
}
同步关系
线程之间的同步关系
为了完成同以目标,需要线程之间按照一定是顺序来执行,不仅是为了保证正确性,也是为了提高效率。 |
条件变量
在linux 操作系统下实现了用条件变量实现进程间同步关系
这里用球员之间传球和投篮之间的同步关系来说明
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
//************简单同步问题***********************
//投篮和传球·
pthread_cond_t g_cond;//条件变量
pthread_mutex_t mutex;//互斥量
void * Pass(void * arg)
{
(void )arg;
while(1)
{
pthread_cond_wait(&g_cond,&mutex);//等待投篮的人
printf("传球\n");
sleep(1);
}
return NULL;
}
void * Shoot(void * arg)
{
(void )arg;
while(1)
{
printf("投篮\n");
sleep(1);
pthread_cond_signal(&g_cond);//告知传球的人可以进行传球了
sleep(2);
}
return NULL;
}
void test()
{
//初始化互斥量
pthread_mutex_init(&mutex,NULL);
//初始化条件变量
pthread_cond_init(&g_cond,NULL);
pthread_t thread_1,thread_2;
//创建两个线程,分别完成传球和投篮任务
pthread_create(&thread_1,NULL,Shoot,NULL);
pthread_create(&thread_2,NULL,Pass,NULL);
//进行线程等待
pthread_join(thread_1,NULL);
pthread_join(thread_2,NULL);
//销毁互斥量
pthread_mutex_destroy(&mutex);
//销毁条件变量
pthread_cond_destroy(&g_cond);
}
int main()
{
test();
return 0;
}
若想更深入了了解同步互斥问题
可以参考一下:
经典互斥问题—生产者消费者模型
经典同步问题—读者写着模型
什么是死锁
在我们解决互斥问题时,我们会在临界区加上锁,那么就会存在这样的问题,当一个线程已经获取了锁,还没有进行释放该锁,又尝试获取再次获取锁,很明显这把锁已经被自己占用了,还没有来的及释放,再次获取锁时一定会阻塞,直到等到锁,那么,既不能释放拥有的锁,也不可获得当前的锁,该线程就会一直阻塞,我们称类似于这种状态为死锁状态。
造成死锁的原因
进程没有及时的释放锁 1. 一个进程尝试获取两次锁 2. 尝试交叉式获取锁,n个进程n把锁,都尝试获取对方的锁(哲学家就餐问题) |
线程安全函数
线程安全函数:多个线程调用该函数不会出现任何逻辑错误
之前我们讲过,可重入函数可重入函数
可重入函数:在不同的执行流中调用该函数不会出现逻辑错误
这里的不同执行流,不仅包含线程,还包含信号处理函数,所以可重入函数要求更严格
可重入函数一定是线程安全函数
线程安全函数不一定是可重入的
一个线程安全但不可重入的例子
#include <stdio.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
//定义一个全局的互斥量
pthread_mutex_t mutex;
//定义一个全局的变量
int count = 5;
//下面的函数是线程安全函数,但不是可重入函数
void pthread_security_function()
{
while(1)
{
//上锁
pthread_mutex_lock(&mutex);
if(count < 0)
{
exit(1);
}
sleep(3);
printf("count: %d\n",count);
sleep(1);
count--;
//解锁
pthread_mutex_unlock(&mutex);
}
}
//线程入口函数
void * Entry(void * arg)
{
(void)arg;
pthread_security_function();
return NULL;
}
//信号处理函数
void sig_entry(int sig)
{
//在信号处理函数中进行调用一个线程安全函数
(void)sig;
pthread_security_function();
}
void test()
{
//对互斥量进行初始化
pthread_mutex_init(&mutex,NULL);
//信号捕捉
signal(SIGINT,sig_entry);
//创建线程
pthread_t tid_1,tid_2;
pthread_create(&tid_1,NULL,Entry,NULL);
pthread_create(&tid_2,NULL,Entry,NULL);
//线程等待
pthread_join(tid_1,NULL);
pthread_join(tid_2,NULL);
//销毁互斥量
pthread_mutex_destroy(&mutex);
}
int main()
{
test();
return 0;
}
执行结果: