linux线程安全篇ⅱ(代码片段)

Suk-god Suk-god     2022-12-05     303

关键词:

文章目录

0、接上篇

线程安全

1、同步存在的必要性

1.1 样例引入

有了互斥之后,为什么还要有同步呢?
这个问题值得我们讨论,我们知道,互斥通过控制线程的访问时序从而保证线程的安全。既然线程已经是安全的了,那还有同步什么事情呢?
我们通过一个例子慢慢体会一下:

现在有一个这样的场景:
有两个人,A和B,有一只碗,A一直向碗中做面,B一直从碗里吃面。

这种情况,我们看到的结果应该要是A做一碗,B吃一碗

将上述情景翻译为线程相关的描述就是:
有一个全局的临界资源g_bowl,其中g_bowl = 1表示有面, g_bowl = 0表示没面。有两个线程A和BA是一个做面线程,只负责向碗里做面,B是一个吃面线程,只负责从碗里吃面。

我们想得到的结果:碗里没面,A去做,碗里有面,B去吃!

好的,我们通过代码模拟一下上面的场景:

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>

#define THREAD_COUNT 1

int g_bowl = 0;//0-->没有面  1-->有面
pthread_mutex_t g_lock;


void* eat_thread_start(void* arg)

    while(1)
    
        //加锁
        pthread_mutex_lock(&g_lock);
        printf("I am eat thread: eat %d\\n",g_bowl--);
        //解锁
        pthread_mutex_unlock(&g_lock);
        usleep(1);
    


void* make_thread_start(void* arg)

    while(1)
    
        //加锁
        pthread_mutex_lock(&g_lock);
        printf("I am make thread: make %d\\n",++g_bowl);
        //解锁
        pthread_mutex_unlock(&g_lock);
        usleep(1);
    




int main()

    //初始化互斥锁
    pthread_mutex_init(&g_lock,NULL);

    pthread_t eat[THREAD_COUNT],make[THREAD_COUNT];
    //创建做面和吃面的线程
    for(int i = 0; i < THREAD_COUNT; i++)
    
        int ret = pthread_create(&eat[i], NULL, eat_thread_start, NULL);
        if(ret < 0)
        
            perror("pthread_create");
            return 0;
        

        ret = pthread_create(&make[i], NULL, make_thread_start, NULL);
        if(ret < 0)
        
            perror("pthread_create");
            return 0;
        
    

    //主线程等待工作线程
    for(int i = 0; i < THREAD_COUNT; i++)
    
        pthread_join(eat[i],NULL);
        pthread_join(make[i],NULL);
    

    //销毁互斥锁
    pthread_mutex_destroy(&g_lock);

    return 0;


输出结果:

从上面的例子我们可以得到这样一个信息:

1、仅仅有互斥是不能够满足我们的程序需求的
2、线程访问临界区代码存在不合理性

1.2 结论

1、多个线程保证了互斥,也就保证了线程能够独占访问临界资源了。但是,并不能保证线程访问临界资源的合理性
2、同步存在的意义就是保证多个线程对临界资源访问的合理性,这个合理性建立在互斥的基础上

那如何实现同步呢?我们可以通过判断的方式解决:
🆗,我们现在将上面的代码更改一下,通过判断的方式,保证线程访问临界资源的合理性~具体的思路如下图:
图解:

我们实现一下代码,其实很简单,只需要在两个线程的入口函数内部加上判断即可:

好的,我们来看一下运行结果:

我们发现结果貌似和我们想要的是一致了。但是这样的代码有什么问题没有?
我们结合代码来分析一下:

运行结果如下:

要想解决这个问题,我们就需要了解条件变量,使用条件变量接口来解决,而不是简单的判断解锁~

2、条件变量

2.1 条件变量的原理 && 使用

原理:
 条件变量本质上是一个PCB等待队列,存放着等待的线程的PCB
使用:
 线程在加锁之后,先判断临界资源是否可用,如果可用,直接访问临界资源,如果不可用,调用条件变量的等待接口,让线程进行等待。

2.3 条件变量的接口

初始化接口

  1. 动态初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t * restrict attr);
参数

cond:条件变量的指针(地址)
attr:表示条件变量的属性信息,一般传递NULL,表示使用默认属性

返回值

初始化成功 ---- 返回0
初始化失败-----设置errno并返回

  1. 静态初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER

等待接口

哪个线程调用等待接口,就将哪个线程放到条件变量对应的PCB等待队列中去

int pthread_cond_wait(pthread_cond_t* restrict cond,pthread_mutex_t *restrict mutex)
参数

cond:条件变量的指针(地址)
mutex:互斥锁

唤醒接口

int pthread_cond_broadcast(pthread_cond_t * cond)
唤醒PCB等待队列当中的所有线程
int pthread_cond_signal(pthread_cond_t * cond)
唤醒PCB等待队列当中的至少一个线程

销毁接口

将条件变量进行销毁
int pthread_cond_destroy(pthread_cond_t* cond)

有了这些知识,我们再次对上面的代码做出如下修改:

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>

#define THREAD_COUNT 1

int g_bowl = 0;//0-->没有面  1-->有面
pthread_mutex_t g_lock;
pthread_cond_t g_cond;

void* eat_thread_start(void* arg)

    while(1)
    
        //加锁
        pthread_mutex_lock(&g_lock);
        if(g_bowl == 0)
        
            printf("我是吃面人,碗里没有面,我就不吃了~~~\\n");
            pthread_cond_wait(&g_cond,&g_lock);
        
        printf("I am eat thread: eat %d\\n",g_bowl--);
        //解锁
        pthread_mutex_unlock(&g_lock);

        //通知做面线程做面
        pthread_cond_signal(&g_cond);
    


void* make_thread_start(void* arg)

    while(1)
    
        //加锁
        pthread_mutex_lock(&g_lock);
        if(g_bowl == 1)
        
            printf("我是做面人,碗里有面,我就不做了~~~\\n");
            pthread_cond_wait(&g_cond,&g_lock);
        

        printf("I am make thread: make %d\\n",++g_bowl);
        //解锁
        pthread_mutex_unlock(&g_lock);
        //通知吃面线程吃面
        pthread_cond_signal(&g_cond);
    




int main()

    //初始化互斥锁
    pthread_mutex_init(&g_lock,NULL);
    //初始化条件变量
    pthread_cond_init(&g_cond,NULL);

    pthread_t eat[THREAD_COUNT],make[THREAD_COUNT];
    //创建做面和吃面的线程
    for(int i = 0; i < THREAD_COUNT; i++)
    
        int ret = pthread_create(&eat[i], NULL, eat_thread_start, NULL);
        if(ret < 0)
        
            perror("pthread_create");
            return 0;
        

        ret = pthread_create(&make[i], NULL, make_thread_start, NULL);
        if(ret < 0)
        
            perror("pthread_create");
            return 0;
        
    

    //主线程等待工作线程
    for(int i = 0; i < THREAD_COUNT; i++)
    
        pthread_join(eat[i],NULL);
        pthread_join(make[i],NULL);
    

    //销毁互斥锁
    pthread_mutex_destroy(&g_lock);
    //销毁条件变量
    pthread_cond_destroy(&g_cond);
    return 0;


分析代码:

运行结果:

但是,对于多个吃面线程和多个做面线程是否依旧成立呢?我们将县城徐良更改后看一下现象:

让程序跑起来,通过pstack查看线程数量,确保创建的线程符合我们的预期

一共有5个线程,一个主线程和4个工作线程。4个工作线程由2个做面线程和2个吃面线程组成。符合我们的预期

分析运行结果:

为什么会出现这样的现象,是值得我们深思的地方。要想理清楚这里面的缘由,我们需要先搞清楚pthread_cond_wait这个接口都做了些什么工作

2.4 条件变量夺命追问

条件变量的等待接口第二个参数为什么会有互斥锁

前提:在线程访问临界资源之前,一定是加锁访问的,因为要保证互斥!
将互斥锁传递给pthread_cond_wait接口,目的就是让该接口在内部执行解锁操作

为什么要解锁呢?通过下面的分析你可以得到答案

pthread_cond_wait的内部针对互斥锁做了什么操作?先释放互斥锁还是先将线程放到PCB等待队列?

1、pthread_cond_wait接口在内部对互斥锁进行了解锁操作
2、先将线程放到PCB等待队列,再释放互斥锁

执行先后的原因如下:

对于上图的两种情况,分别进行分析:

因此,情况二才是正确的。即先将线程放到PCB等待队列,再对互斥锁进行解锁

线程被唤醒之后会执行什么代码?

pthread_cond_wait函数在被唤醒之后一定会在其内部进行加锁操作

当然,加锁的权限和其他不在PCB等待队列中的线程是一样的,也就是说,它并不一定就能够抢到锁。

1、抢到锁了:pthread_cond_wait函数就真正执行完毕了,函数返回
2、没有抢到锁:pthread_cond_wait函数没有真正的执行完毕,还处于内部抢锁的逻辑当中,还会继续去抢锁,直到抢到互斥锁后才返回

2.5 条件变量的代码

好的,我们在了解了pthread_cond_wait接口更多细节之后,我们来分析一下,为什么吃面线程和做面线程由1个变为2个之后,程序就出错了呢?

分析的前提场景如下图:

下面描述的场景是有可能存在的,由于线程间是抢占式执行的,因此在分析问题的时候常常使用假设的方式:

那么,我们应该如何更改这个代码呢?
只需要将线程入口函数处的条件判断if更改为while,在pthread_cond_wait接口退出的时候就会循环上去判断临界资源是否可用,进而保证了临界资源访问的合理性
具体代码如下:

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>

#define THREAD_COUNT 2

int g_bowl = 0;//0-->没有面  1-->有面
pthread_mutex_t g_lock;
pthread_cond_t g_cond;

void* eat_thread_start(void* arg)

    while(1)
    
        //加锁
        pthread_mutex_lock(&g_lock);
        while(g_bowl == 0)
        
            printf("我是吃面人%p,碗里没有面,我就不吃了~~~\\n",pthread_self());
            pthread_cond_wait(&g_cond,&g_lock);
        
        printf("我是吃面人%p: eat %d\\n",pthread_self(),g_bowl--);
        //解锁
        pthread_mutex_unlock(&g_lock);

        //通知做面线程做面
        pthread_cond_signal(&g_cond);
    


void* make_thread_start(void* arg)

    while(1)
    
        //加锁
        pthread_mutex_lock(&g_lock);
        while(g_bowl == 1)
        
            printf("我是做面人%p,碗里有面,我就不做了~~~\\n",pthread_self());
            pthread_cond_wait(&g_cond,&g_lock);
        

        printf("我是做面人%p: make %d\\n",pthread_self(),++g_bowl);
        //解锁
        pthread_mutex_unlock(&g_lock);
        //通知吃面线程吃面
        pthread_cond_signal(&g_cond);
    



int main()

    //初始化互斥锁
    pthread_mutex_init(&g_lock,NULL);
    //初始化条件变量
    pthread_cond_init(&g_cond,NULL);

    pthread_t eat[THREAD_COUNT],make[THREAD_COUNT];
    //创建做面和吃面的线程
    for(int i = 0; i < THREAD_COUNT; i++)
    
        int ret = pthread_create(&eat[i], NULL, eat_thread_start, NULL);
        if(ret < 0)
        
            perror("pthread_create");
            return 0;
        

        ret = pthread_create(&make[i], NULL, make_thread_start, NULL);
        if(ret < 0)
        
            perror("pthread_create");
            return 0;
        
    

    //主线程等待工作线程
    for(int i = 0; i < THREAD_COUNT; i++)
    
        pthread_join(eat[i],NULL);
        pthread_join(make[i],NULL);
    

    //销毁互斥锁
    pthread_mutex_destroy(&g_lock);
    //销毁条件变量
    pthread_cond_destroy(&g_cond);
    return 0;


运行结果

但是,我们又发现了一个问题,程序运行一段时间就卡死了,也不会退出!
遇到这种情况,我们先通过pstack来查看一下每个线程的状态,进而分析程序出错的原因

为什么会出现所有的工作线程都处于PCB等待队列中呢?
原因是,在线程通知PCB等待队列中的线程的时候,将同种类的线程通知出来了,然后判断临界资源是不可用状态,因此刚被通知出来的线程什么工作也没有做,就又进入PCB等待队列中了。

那因该如何解决这种问题呢?
 只需要将吃面线程和做面线程的条件变量分开就可以了。这样吃面线程通知的永远是做面线程条件变量对应的PCB等待队列,做面线程通知的永远是吃面线程条件变量对应的PCB等待队列。这样就不会出现上述的情况了!

🆗,理解到这里我们重新更改一下代码:
这里的代码已经是终极版本了,代码解决了多线程的安全性问题以及对临界资源的访问合理性问题!!

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>

#define THREAD_COUNT 2

int g_bowl = 0;//0-->没有面  1-->有面
pthread_mutex_t g_lock;
pthread_cond_t g_eat_cond;//吃面线程的条件变量
pthread_cond_t g_make_cond;//做面线程的条件变量

void* eat_thread_start(void* arg)

    while(1)
    
        //加锁
        pthread_mutex_lock(&g_lock);
        while(g_bowl == 0)
        
            printf("我是吃面人%p,碗里没有面,我就不吃了~~~\\n",pthread_self());
            pthread_cond_wait(&g_eat_cond,&g_lock);
        
        printf("我是吃面人%p: eat %d\\n",pthread_self(),g_bowl--);
        //解锁
        pthread_mutex_unlock(&g_lock);

        //通知做面线程做面
        pthread_cond_signal(&g_make_cond);
    


void* make_thread_start(void* arg)

    while(1)
    
        //加锁
        pthread_mutex_lock(&g_lock);
        while(g_bowl == 1)
        
            printf("我是做面人%p,碗里有面,我就不做了~~~\\n",pthread_self());
            pthread_cond_wait(&g_make_cond,&g_lock);
        

        printf("我是做面人%p: make %d\\n",pthread_self(),++g_bowl);
        //解锁
        pthread_mutex_unlock(&g_lock);
        //通知吃面线程吃面
        pthread_cond_signal(&g_eat_cond);
    



int main()

    //初始化互斥锁
    pthread_mutex_init(&g_lock,NULL);
    //初始化条件变量
    pthread_cond_init(&g_eat_cond,NULL);
    pthread_cond_init(&g_make_cond,NULL);
    pthread_t eat[THREAD_COUNT],make[THREAD_COUNT];
    //创建做面和吃面的线程
    for(int i = 0; i < THREAD_COUNT; i++)
    
        int ret = pthread_create(&eat[i], NULL, eat_thread_start, NULL);
        if(ret < 0)
        
            perror("pthread_create");
            return 0;
        

        ret = pthread_create(&make[i], NULL, make_thread_start, NULL);
        if(ret < 0)
        
            perror("pthread_create");
            return 0;
        
    

    //主线程等待工作线程
    for(int i = 0; i < THREAD_COUNT; i++)
    
        pthread_join(eat[i],NULL);
        pthread_join(make[i],NULL);
    

    //销毁互斥锁
    pthread_mutex_destroy(&g_lock);
    //销毁条件变量
    pthread_cond_destroy(&g_eat_cond);
    pthread_cond_destroy(&g_make_cond);
    return 0;
查看详情  

java并发/多线程系列——线程安全篇

创建和启动Java线程Java线程是个对象,和其他任何的Java对象一样。线程是类的实例java.lang.Thread,或该类的子类的实例。除了对象之外,java线程还可以执行代码。创建和启动线程在Java中创建一个线程是这样完成的:Threadthread=newTh... 查看详情

我的安全之路——web安全篇

writeinmydormitoryat‏‎9:47:05Friday,April7,2017bygiantbranch(其实当初想横跨web跟二进制的)————致即将毕业的自己。这是我的安全之路系列第一篇,敬请期待第二篇:《我的安全之路——二进制与逆向篇》总览大一&#x... 查看详情

业务逻辑漏洞挖掘随笔信息一致性安全篇

继续~1手机号篡改  场景一:抓包修改手机号码参数为其他号码尝试,例如在办理查询页面,输入自己的号码然后抓包,修改手机号码参数为其他人号码,查看是否能查询其他人的业务。  场景二:在测试一个业务的时,第... 查看详情

浅谈http(简)https链接安全篇

前言:我们带着问题去思考?为什么HTTPS是安全的?它是如何保证安全的?1.HTTPS简介其实很多时候,HTTP网站会出现很多的小广告,对网站进行遮挡或者严重影响网站的美观,当然这只是从直观上看来能... 查看详情

项目案例第二篇中小型公司优化性能安全篇

公司拓扑:现在这公司出现的问题:1)网络内经常出现地址冲突2)广播流量过大3)网速不容乐观4)网络故障频繁5)故障恢复时效低分析解决方案:1)网络内出现地址冲突,根据每个部门所需ip地址数量,多做冗余2)禁用vtp,... 查看详情

用dedecms搭建网站的安全篇:默认模板路径漏洞

...享一个DEDECMS默认模板路径,用DEDECMS搭建的网站必看的安全篇。织梦CMS是集简单、健壮、灵活、开源几大特点的开源内容管理系统,是国内开源CMS的领先品牌,目前程序安装量已达七十万,超过六成的站点正在使用织梦CMS或基于织... 查看详情

前端面试题整理—性能优化及安全篇

1、什么是web语义化,以及他的优势web语义化是指通过HTML标记表示页面包含的信息,包含了HTML标签的语义化和css命名的语义化HTML标签的语义化是指:通过使用包含语义的标签(如h1-h6)恰当地表示文档结构css命名的语义化是指:... 查看详情

3-stm32物联网开发wifi(esp8266)+gprs(air202)系统方案安全篇(购买域名,域名绑定ip)

 2-STM32物联网开发WIFI(ESP8266)+GPRS(Air202)系统方案安全篇(监听Wi-Fi和APP的数据) 因为安全连接是和域名绑在一块的,所以需要申请域名有没有不知道域名是什么的,但是大家一定知道访问域名就是访问绑定在域名上的IP地址域名... 查看详情

分享几款linux安全运维工具(代码片段)

1.查看进程占用带宽情况-NethogsNethogs是一个终端下的网络流量监控工具可以直观的显示每个进程占用的带宽。root@localhost~]#yum-yinstalllibpcap-develncurses-devel[root@localhost~]#tarzxvfnethogs-0.8.0.tar.gz[root@localhost~]#cdnethogs[r 查看详情

python的线程06认识线程安全文末送书(代码片段)

...ff0c;别错过这个从0开始的文章!前面学委分享了5篇多线程的文章了,一开始写多线程程序好像非常简单。可是实际应用跟第4篇,第5篇的场景比较像,而且还更复杂。有没有安全方法进行多线程编程?我们先... 查看详情

nightmareⅱ(双向bfs)(代码片段)

ProblemDescriptionLastnight,littleerriyuehadahorriblenightmare.Hedreamedthatheandhisgirlfriendweretrappedinabigmazeseparately.Moreterribly,therearetwoghostsinthemaze.Theywillkillthepeople.Nowlittleerr 查看详情

hdu3085nightmareⅱ(代码片段)

题目:Lastnight,littleerriyuehadahorriblenightmare.Hedreamedthatheandhisgirlfriendweretrappedinabigmazeseparately.Moreterribly,therearetwoghostsinthemaze.Theywillkillthepeople.Nowlittleerriyuewantstoknow 查看详情

linux下将java程序安装为服务自启动(代码片段)

测试环境是centos7,其他版本请自测原理是安装为systemctl服务,可以实现开机自启动,异常关闭自动重启,可以省不少事情。下面直接上shell命令,复制保存为sh文件,修改最上面的变量,然后运行就可以了#!/bin/bashstartshell=\'/home/s... 查看详情

打家劫舍ⅰ&ⅱ(代码片段)

打家劫舍Ⅰ&Ⅱ打家劫舍Ⅰ题目代码打家劫舍Ⅱ题目代码打家劫舍Ⅰ题目代码classSolutionpublic:introb(vector<int>&nums)if(nums.size()==0)return0;if(nums.size()==1)returnnums[0];vector<int>dp(nums.size()& 查看详情

linux下将java程序安装为服务自启动(代码片段)

测试环境是centos7,其他版本请自测原理是安装为systemctl服务,可以实现开机自启动,异常关闭自动重启,可以省不少事情。下面直接上shell命令,复制保存为sh文件,修改最上面的变量,然后运行就可... 查看详情

acm_走楼梯ⅱ(代码片段)

走楼梯ⅡTimeLimit:2000/1000ms(Java/Others)ProblemDescription:有一楼梯共N+1级,刚开始时你在第一级,若每次能走M级(1<=M<=N),要走上第N+1级,共有多少种走法?(不可以后退)Input:输入可能包含多个测试样例,对于每个测试案例,输入... 查看详情

《windows程序设计》滚动条ⅱ(08)(代码片段)

代码如下:programProject2;$APPTYPECONSOLE$R*.resusesSystem.SysUtils,windows,Winapi.Messages,Vcl.Dialogs;varswndClass:tagWNDCLASS;message:MSG;mHwnd:hwnd;cxChar,cyChar:Integer;cxClient,cyClient:Integer;S 查看详情