线程互斥
线程互斥的产生条件
在了解线程的基本知识后,我们知道,对于线程而言,所有线程共享进程的地址空间。在进程中的文件描述符,信号处理方式,定义的全局变量,所有线程都可以看到。线程通过共享这些资源的方式,完成线程间的交互。
我们把这些可以共享的资源叫做临界资源。程序访问临界资源的代码区,叫做临界区。
那么当多个线程并发的去操作临界资源时,就会引发一些问题。比如访问一个全局变量。就会导致数据不一致问题。
为了证明我们所说的问题,来看下面代码(模拟买票系统)
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
int ticket=100;
void* route( void *arg)
{
char *id=( char*)arg;
while( 1)
{
if(ticket>0 )
{
sleep(1);
printf( "%s sells ticket %d\n",id,ticket);
ticket--;
}
else
{
break;
}
}
}
int main( )
{
//创建4个线程,模拟四个用户买票
pthread_t t1,t2,t3,t4;
pthread_create(&t1,NULL,route,"thread_1");
pthread_create(&t2,NULL,route,"thread_2");
pthread_create(&t3,NULL,route,"thread_3");
pthread_create(&t4,NULL,route,"thread_4");
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_join(t3,NULL);
pthread_join(t4,NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
在这个例子中,票数ticket就是一个临界资源,当几个进程同时访问他时,结果就出现了图中标注的结果。下面来分析原因
- ticket是非原子操作
对ticket减 1操作,对应汇编代码,是三条语句
1.load 将共享变量从内存加载道寄存器
2 updata 更新寄存器里的值,进行减1操作
3 store 将新值从寄存器加载到内存中
在这三步操作中,由于不是原子操作,就会导致并没有执行完这三步操作,又会重新开始执行的问题,从而引发数据不一致问题。比如,当一个线程1,开始操作ticket变量(假设ticket的初始值为100),执行到第二步,将ticket减1,这时,由于线程切换,线程2开始运行,也访问ticket。此时,线程1刚刚对ticket减1,将ticket变成99,还没有来得及放回内存,线程2就进入这个临界区了。所以线程1不得不把99先保存起来。让出CPU给线程2。
线程2争取到CPU资源时,也对ticke减一操作。对于线程2,以及后面的线程,他们访问ticket时,都执行了完整的操作,即三步全部执行完,然后让出资源。知道ticket变成1,
当其他线程访问完ticket时,这时线程1被切回来了,他刚刚执行到第二步,他眼中的ticket是99。他将继续执行第三步,将99放回内存中去。
可是,现在内存中的ticket是1,(线程2,3.,4执行的结果)。对于同一个变量,既是1,又是99,这是不可能的。所以就出现了前面所说的数据不一致问题。
- if条件为真,代码可以并发的切换到其他线程
- sleep()这个模拟业务的漫长过程中,可以有很多线程进入临界区
要解决这三个 问题,需要做到以下三点:
代码必须要有互斥行为:当代码进入临界区行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
互斥锁实现原理:
设置一个共享(锁)变量,其初始值为0.当一个线程进入临界区时,他首先测试这把锁。如果该锁的值是0,则该线程就将锁的值设置成1,并进入临界区。若这把锁的值是1,。则该进程将进行等待直到锁的值变为0。于是0就表示了临界区没有线程,1表示了某个线程进入了临界区。
互斥锁
初始化互斥量有两种方法:
方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread mutexattr _t *restrict attr);
参数:mutex:要初始化的互斥量
attr:NULL
销毁互斥量
销毁互斥量需要注意:
- 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread mutex t *mutex); int pthread_mutex_unlock(pthread mutex t *mutex);
返回值:成功返回0,失败返回错误号
调⽤用pthread_ lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread lock调用会陷阻塞,等待互斥量解锁。
基于上面背景知识,我们现在对刚才的售票系统进行改进。给他加上互斥锁(只是处理此问题的方法之一)
#1include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
int ticket=100;
//定义一把锁
pthread_mutex_t mutex;
void* route( void *arg)
{
char *id=( char*)arg;
while( 1)
{
//加锁
pthread_mutex_lock(&mutex);
if(ticket>0 )
{
sleep(1);
printf( "%s sells ticket %d\n",id,ticket);
ticket--;
//释放锁
pthread_mutex_unlock(&mutex);
}
else
{
//释放锁
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main( )
{
//初始化锁
pthread_mutex_init(&mutex,NULL);
pthread_t t1,t2,t3,t4;
pthread_create(&t1,NULL,route,"thread_1");
pthread_create(&t2,NULL,route,"thread_2");
pthread_create(&t3,NULL,route,"thread_3");
pthread_create(&t4,NULL,route,"thread_4");
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_join(t3,NULL);
pthread_join(t4,NULL);
//销毁锁
pthread_mutex_destroy(&mutex);
return 0;
}
现在就解决了多线程访问临界资源出现的数据不一致问题。根据代码,可以用一张图简明说明互斥机制(在上面代码中,我们对临界资源实现了加锁机制)
在图中,线程1在T1时刻申请到锁进入临界区。稍后,T2时刻,线程2尝试申请锁进入临界区。但是失败了,因为此时另一个线程已经在临界区,并没有解锁。所以线程2没有锁,但是他又要访问临界区资源,所以此时他就被暂时挂起,直到T3时刻线程1释放锁,离开临界区为止。线程2申请到锁,进入临界区。最后在T4时刻,B离开临界区。临界区就暂时处于没有线程进入的临时状态。
线程同步机制
在前面的例子中,我们解决了共享变量不一致的问题。
下面来进一步分析该问题。
我们知道,当为防止多个线程同时进入临界区引发数据不一致问题,可以给临界资源加上一把锁。线程申请到锁才有可能访问到临界资源。而其他没有申请锁的线程则会一直被挂起知道锁被释放。
我们知道,进程有优先级,线程也有优先级。当某个线程优先级很高,只要这个优先级很高的线程一到,其他线程都必须排到后面挂起等待。而这个高贵的线程就像一个纨绔的富家子弟一样。申请到锁,进入临界区,却然后又释放锁,如此回环往复。导致其他线程一直得不到临界资源。从而引发饥饿问题,无法完成任务。
比如在一个数据生产者线程和消费数据线程,消费者线程一直占用着互斥锁,数据消费完了依然霸占着锁。这样就会导致数据生产者无法生产数据,而消费者在消费完有限的数据后,也无法再正常消费。
为了解决这个问题,我们需要引入同步机制。实现线程与线程之间协同访问临界资源。就比如让生产者与消费者都可以访问到临界资源。正常工作。
线程同步方法
- 条件变量
- POSIX信号量
本文先介绍用条件变量实现线程同步
先来认识条件变量的相关接口
- 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread condattr t *rest rict attr);
参数:
cond:要初始化的条件变量attr:NULL
- 销毁
int pthread_cond_destroy(pthread_cond_t *cond)
- 等待条件满足
int pthread_cond_wait(pthread cond t *restrict cond,pthread mutex_t *restrict mute x);
参数:cond:要在这个条件变量上等待mutex:
互斥量,后面详细解释
等待的时候必须释放锁,否则会造成死锁问题。
必须在指定的mutex互斥锁下面等待,且必须等待。
所谓等待,其实就是把pcb连接到条件变量的函数体内,并将进程状态从R状态变成非R状态。
- 唤醒等待
唤醒所有线程
int pthread_cond_broadcast(pthread cond_t *cond);
唤醒指定线程
int pthread_cond_signal(pthread cond_t *cond);
下面来看简单运用
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
//定义一个互斥锁和条件变量
pthread_mutex_t mutex;
pthread_cond_t cond;
//线程1的执行任务
void* r1( void *arg)
{
while(1)
{
//等待被唤醒后执行任务
pthread_cond_wait(&cond,&mutex);
printf("hello\n");
}
}
//线程2的执行任务
void* r2(void *arg)
{
while( 1)
{
//每隔1s唤醒线程1,提示可以执行任务了
pthread_cond_signal(&cond);
sleep(1);
}
}
int main( )
{
//初始化互斥锁
pthread_mutex_init(&mutex,NULL);
//初始化条件变量
pthread_cond_init(&cond,NULL);
pthread_t t1,t2;
//创建线程1,线程2
pthread_create(&t1,NULL,r1,"thread_1");
pthread_create(&t2,NULL,r2,"thread_2");
//等待线程
pthread_join(t1,NULL);
pthread_join(t2,NULL);
//销毁互斥锁和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
从结果可以看出,线程1,只有在接收到线程2的通知,才开始执行任务。打印hello.
以上就是线程的互斥与同步机制简单介绍。在接下来的博客里,来实现基于同步互斥的生产者消费者模型。