博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
27-线程同步——互斥量
阅读量:2042 次
发布时间:2019-04-28

本文共 6167 字,大约阅读时间需要 20 分钟。

1. 银行存款问题

  来看一个银行存款问题,比如在银行里存了6000块钱,模拟同一时刻有2个人都去银行取钱,每个人每次只取1000块钱,我们来看一下这会发生什么情况。

#include 
#include
#include
#include
#include
int money = 6000; //银行存款(共享数据)//线程主控函数void *tfn(void *arg){
int num = (int)arg; //随机数 srand(time(NULL)); while(1){
if(money <= 0){
return (void *)0; } if(num == 0){
printf("pthread1 get money 1000\n"); /*模拟长时间操作共享资源,导致cpu易主,产生错误*/ sleep(rand()%3); money -= 1000; } if(num == 1){
printf("pthread2 get money 1000\n"); sleep(rand()%3); money -= 1000; } printf("money剩余:%d\n" , money); } return (void *)0;}int main(void){
pthread_t tid[2]; int ret; int i; //创建两个线程 for(i=0; i<2; ++i){
ret = pthread_create(&tid[i],NULL,tfn,(void *)i); if(ret != 0){
perror("pthread_create"); return -1; } } //回收线程 for(i = 0; i<2; ++i){
pthread_join(tid[i], NULL); } return 0;}

程序执行结果:

在这里插入图片描述

  我们发现了一个很有意思的现象,银行总共存了6000块钱,这时有2个人去取钱,按理说这两个人加起来只能取到6000块,但是程序中的结果是2个人加起来取到了7000块钱。

分析出错原因:

  存款是一个共享的数据,两个人都可以去银行取钱,比如A去银行取了1000块钱,对应的银行系统在存款中减去了1000, 这一操作是需要时间的 ,那我们来想象一下,如果过程中发生了一些特殊情况,比如银行系统突然响应很慢,还没来得及减去1000,这时B又来银行取钱了,然后B也取走了1000块钱,这时银行系统才在存款中减去了1000,显然系统中的数据是有问题的,银行不可能允许发生这种事情的,要不然就亏大了。

  也就是说,当一个线程访问共享数据时,其他线程都有可能对共享数据进行操作,如果不对这种情况进行相应的处理,那么程序的执行结果将是不可预测的。

2. 线程同步

根据银行存款问题,出现数据混乱的原因主要有以下几个:

  1. 资源共享
  2. 线程调度随机
  3. 线程间缺乏必要的同步机制

  以上这几点中前两点不能改变,为了避免数据混乱的问题,就只能从第三点着手解决,需要引入线程同步机制,那么什么是线程同步?

  简单来说,当多个线程共享同一内存时,需要确保共享的数据同一时刻只能有一个线程访问,且当前线程会对共享数据加一把互斥锁,在当前访问期间,其他线程都不能对共享数据进行任何操作,只有当线程访问完毕解锁后,其他线程才有可能对共享数据进行操作,这一过程就是同步。

3. 多线程互斥

  linux中的pthread库提供一把互斥锁mutex(也称之为互斥量),互斥锁是用加锁的方式来控制对共享资源的操作。这个互斥锁只有上锁和解锁两种状态,在同一时刻只能有一个线程掌握互斥锁,也只能拥有锁的线程才能对共享资源进行操作。

  如果其他线程希望对一个已经被上锁的互斥锁进行上锁的话,则该线程会挂起阻塞,直到上锁的线程释放掉互斥锁为止。资源还是共享的,线程间也还是竞争的,但互斥锁保证让每个线程对共享资源进行原子操作。通过这种方式就不会出现银行存款的问题了。

4. 互斥量操作函数

多线程互斥函数主要有以下几个函数:

pthread_mutex_init函数pthread_mutex_destroy函数pthread_mutex_lock函数pthread_mutex_trylock函数pthread_mutex_unlock函数

4.1 pthread_mutex_init函数

以pthread_mutex_init函数为例,pthread_mutex_init函数的作用是初始化一把互斥锁。

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

返回值:成功返回0,失败返回错误

参数说明:

  参数mutex:表示初始化一个互斥量

  restrict关键字是c99引入的,是C语言中一种类型限定符,只用于限制指针,告诉编译器,只有mutex指针指向这块内存,只能通过mutex指针修改这块内存。restrict在pthread_mutex_init函数中修饰mutex的作用是:限制mutex,想要修改mutex指向内存中的内容只能通过mutex修改。

  参数attr:表示设置互斥量的属性,就是设置创建哪一种互斥锁。是一个传入参数,通常传NULL,选用默认的互斥锁(线程间共享),在使用互斥量之前,必须对它进行初始化,初始化方式有静态初始化或动态初始化。

//销毁一个互斥锁int pthread_mutex_destroy(pthread_mutex_t *mutex);//对一个互斥锁进行加锁操作//解锁操作int pthread_mutex_unlock(pthread_mutex_t *mutex);

4.2 lock函数和unlock函数

   pthread_mutex_lock函数是用于加锁操作,每个线程在对资源操作前都尝试先加锁,成功加锁才能操作。

int pthread_mutex_lock(pthread_mutex_t *mutex);

  解锁同理,跟加锁反过来理解,pthread_mutex_unlock函数在解锁的同时还会将阻塞在该锁上的所有线程全部唤醒

int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失败返回非0

   我们看到不管是lock函数还是unlock函数,它们的参数都是pthread_mutex_t 类型,实际上pthread_mutex_t 是互斥量的数据类型,通常是一个结构体,用于定义一个互斥量mutex。为了方便理解,应用时可忽略其实现细节,简单把互斥量当成整数看待,可以认为在调用pthread_mutex_init初始化互斥量mutex时,mutex初值为1,加锁操作为mutex–(mutex值为0),解锁操作为mutex++。

   加锁与解锁:加锁和解锁之间的区域我们也叫临界区(共享资源),线程想要操作这块临界区必须先加锁再操作,操作完毕再解锁。unlock主动解锁的同时还会将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒这取决于操作系统。

5. 使用互斥量解决银行存款问题

#include 
#include
#include
#include
#include
//定义互斥量pthread_mutex_t mutex;//共享数据//这次我们在银行里存了一万int money = 10000; //线程主控函数void *tfn(void *arg){
int num = (int)arg; //随机数 srand(time(NULL)); while(1){
//加锁 pthread_mutex_lock(&mutex); //mutex-- if(money <= 0){
//注意:线程进来的时候加锁了,这里要解锁 pthread_mutex_unlock(&mutex); //mutex++ return (void *)0; } //线程1 if(num == 0){
printf("pthread1 get money 1000\n"); /*模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误*/ sleep(rand()%3); money -= 1000; } //线程2 if(num == 1){
printf("pthread2 get money 1000\n"); sleep(rand()%3); money -= 1000; } printf("money剩余:%d\n" , money); //解锁 pthread_mutex_unlock(&mutex); //mutex++ //让其他线程有时间抢到cpu执行权 sleep(rand()%3); } return (void *)0;}int main(void){
pthread_t tid[2]; int ret; int i; //初始化mutex pthread_mutex_init(&mutex , NULL); //创建两个线程 for(i=0; i<2; ++i){
ret = pthread_create(&tid[i],NULL,tfn,(void *)i); if(ret != 0){
perror("pthread_create"); return -1; } } //回收线程 for(i = 0; i<2; ++i){
pthread_join(tid[i], NULL); } //销毁互斥锁 pthread_mutex_destroy(&mutex); return 0;}

程序执行结果:

在这里插入图片描述

  为了能更加明显看到效果,这次我们在银行里存了1万,并使用互斥量进行同步,最后程序执行的结果正常,两人加起来只能取到1万块钱,说明互斥量保证了同一时刻只能有一个人能取钱成功。

6. pthread_mutex_trylock函数

  顾名思义,就是尝试加锁,加锁失败不会阻塞,而是直接返回出错信息EBUSY。

  如果互斥量正处于未加锁状态,那么调用pthread_mutex_trylock尝试对互斥量加锁会成功,否则,调用pthread_mutex_trylock加锁失败,不会阻塞,返回EBUSY错误信息。

int pthread_mutex_trylock(pthread_mutex_t *mutex);

7. lock,trylock,unlock

  1. lock尝试加锁,如果加锁不成功则线程阻塞
  2. trylock加锁失败不会阻塞,直接返回错误,如EBUSY
  3. unlock主动解锁的同时还会将阻塞在该锁上的所有线程全部唤醒

8. 线程同步总结

  一般来说,同步是不考虑线程的互斥,只强调线程的执行顺序约束。比如在访问数据时,保证线程的执行顺序,即线程1先运行,然后线程2再运行。并不需要考虑线程互斥,即线程1还没访问完数据,紧接着线程2又来访问数据了。

  但实际上,同步是建立在线程互斥的基础上,还保证了线程的执行顺序,强调线程的执行顺序和线程互斥。比如在访问数据时,保证线程互斥,即线程1在访问数据时,然后线程2阻塞等待线程1访问完毕。同时还需要保证线程执行顺序,即线程1先运行,然后线程2再运行。

你可能感兴趣的文章
【虫师】【selenium】参数化
查看>>
【JMeter】如何用JMeter进行压力测试
查看>>
【Python练习】文件引用用户名密码登录系统
查看>>
学习网站汇总
查看>>
【Jmeter】如何通过文件导入方式对用户名和密码进行参数化设置
查看>>
【Python】用Python打开csv和xml文件
查看>>
【JMeter】集合点的设置
查看>>
硬盘与内存的区别和联系
查看>>
【代码备份】ZJ10086测试环境成功代码备份
查看>>
【Python】【爬虫】如何学习Python爬虫?
查看>>
【Linux】通过top语句可以查看压力测试的实时服务器状态。(可以通过百度Linux top查看相关内容)...
查看>>
【Python】Python基础
查看>>
东风乘用车文件活动视频播放压测脚本备份
查看>>
【Javascript】Windows下Node.js与npm的安装与配置
查看>>
【接口测试】接口概念及Json相关
查看>>
【Python项目篇】【爬妹子图】
查看>>
【Loadrunner】性能测试报告实战
查看>>
【英语】软件测试工程师相关英文词汇
查看>>
如何在python3.5环境下安装BeautifulSoup?
查看>>
python笔记9-多线程Threading之阻塞(join)和守护线程(setDaemon)
查看>>