Linux之线程互斥

目录

一、问题引入

二、线程互斥

1、相关概念

2、加锁保护

1、静态分配

2、动态分配

3、锁的原理

4、死锁

三、可重入与线程安全

1、概念

2、常见的线程不安全的情况

3、常见的线程安全的情况

4、常见不可重入的情况

5、常见可重入的情况

6、可重入与线程安全联系

7、可重入与线程安全区别


一、问题引入

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。

我们来看看下面的多线程抢票系统的代码:

#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <pthread.h>

using namespace std;

int ticket = 100;

void *getticket(void *arg)
{
    char *name = (char *)arg;
    while (true)
    {
        if (ticket > 0)
        {
            usleep(1000);
            cout << name << ":"
                 << " " << ticket << endl;
            ticket--;
        }
        else
            break;
    }
}

int main()
{
    pthread_t tid1, tid2, tid3, tid4;
    pthread_create(&tid1, nullptr, getticket, (void *)"thread 1");
    pthread_create(&tid2, nullptr, getticket, (void *)"thread 2");
    pthread_create(&tid3, nullptr, getticket, (void *)"thread 3");
    pthread_create(&tid4, nullptr, getticket, (void *)"thread 4");

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);

    return 0;
}

这里的ticket变量是一个全局变量,那么它就会被所有线程共享。创建线程后,所有线程访问getticket函数,对其进行了重入,访问ticket并对ticket--。但是,我们发现,票数出现了负数,这完全不符合我们的代码逻辑和想要的结果。这是为什么呢?

首先,程序在编译的时候会被编译成汇编代码, 而在汇编代码中,ticket--操作在我们看来只有一行代码,但是在汇编中它其实分为了三步:1、将ticket值拷入到CPU寄存器中;2、CPU对其进行--操作;3、将结果写回内存。

而我们知道进程是有时间片的,在执行完上面任意一步时,线程可能因为时间片到了而被切换。而这就会造成一些问题。如下图:

线程A先进入,在完成第二步 -- 操作后,因为时间片到了,要被切换出去,99作为上下文数据被保存起来随A一起被切换。线程B进入,因为B的时间片比较长,他把ticket值减到了50并写回了内存后,时间片到了,被切换。线程A再次进入CPU,把上下文恢复,然后接着第3步执行,直接把99写到了内存里面。

线程B明明已经让ticket的值减到了50,结果你个线程A又直接把结果改成了99。这样就出现了数据错乱的现象。

在我们对ticket进行并发访问的时候,由于ticket- - 操作并不是原子的,所以出现了数据不一致的情况。这种情况怎么解决呢?我们接着往下讲。

二、线程互斥

1、相关概念

1、临界资源:多线程执行流共享的资源就叫做临界资源。
2、临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
3、互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
4、原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

2、加锁保护

为了解决上面代码的数据不一致的问题,需要做到三点:

1、代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

2、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

3、如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

而其中最简单的一种方法就是对临界资源进行加锁保护。以达到下面的效果:

定义和初始化锁的函数: 

NAME
       pthread_mutex_destroy, pthread_mutex_init - destroy and initialize a mutex

SYNOPSIS
       #include <pthread.h>

       1、int pthread_mutex_destroy(pthread_mutex_t *mutex);
       2、int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);

       3、pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t 是由原生线程库给用户提供的一个数据类型,就是我们常说的锁。上图的 1和2 是对锁进行局部定义时的销毁和初始化操作,相当于析构函数和构造函数。

上图的 3 是对全局锁或者static静态锁进行初始化的方式。下面我们一一讲解。

加锁和解锁函数:

发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请锁,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁,再去申请锁。

NAME
       pthread_mutex_lock,  pthread_mutex_trylock,  pthread_mutex_unlock  -  lock   and
       unlock a mutex

SYNOPSIS
       #include <pthread.h>

       int pthread_mutex_lock(pthread_mutex_t *mutex);
       int pthread_mutex_trylock(pthread_mutex_t *mutex);
       int pthread_mutex_unlock(pthread_mutex_t *mutex);

1、静态分配

静态分配就是我们 3 对应的对锁定义和初始化的方式。我们使用它对抢票代码进行保护。

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <time.h>
#include <pthread.h>

using namespace std;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int ticket = 100;

void *getticket(void *arg)
{
    char *name = (char *)arg;
    while (true)
    {
        pthread_mutex_lock(&mutex); // 加锁保护,其他线程只能在这阻塞等待,直到拿到锁
        if (ticket > 0)             // 这部分代码只能串行执行
        {
            usleep(rand() % 10000);
            cout << name << ":"
                 << " " << ticket << endl;
            ticket--;
            pthread_mutex_unlock(&mutex); // 访问完临界资源,解锁,
            // 让其他线程能够拿锁访问
        }
        else
        {
            pthread_mutex_unlock(&mutex); // 访问完临界资源,解锁
            // 让其他线程能够拿锁访问
            break;
        }
        usleep(rand() % 2000000);
    }
    return nullptr;
}

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid() ^ 433);
    pthread_t tid1, tid2, tid3, tid4;
    pthread_create(&tid1, nullptr, getticket, (void *)"thread 1");
    pthread_create(&tid2, nullptr, getticket, (void *)"thread 2");
    pthread_create(&tid3, nullptr, getticket, (void *)"thread 3");
    pthread_create(&tid4, nullptr, getticket, (void *)"thread 4");

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);

    return 0;
}

注:加锁的时候,一定要保证加锁粒度越小越好。最好不要让一些非临界区也被加锁保护。

2、动态分配

如果我们定义的锁是一个局部变量,那么我们就要像下面的代码这样使用锁:

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <time.h>
#include <pthread.h>

using namespace std;
#define THREAD_NUM 5

class threaddata
{
public:
    threaddata(const string &s, pthread_mutex_t *m)
        : name(s), mtx(m)
    {}

public:
    string name;
    pthread_mutex_t *mtx;
};

int ticket = 100;

void *getticket(void *arg)
{
    threaddata *td = (threaddata *)arg;
    while (true)
    {
        pthread_mutex_lock(td->mtx);
        if (ticket > 0)              
        {
            usleep(rand() % 10000);
            cout << td->name << ":"
                 << " " << ticket << endl;
            ticket--;
            pthread_mutex_unlock(td->mtx);
        }
        else
        {
            pthread_mutex_unlock(td->mtx);
            break;
        }
        usleep(rand() % 2000000);
    }
    delete td;
    return nullptr;
}

int main()
{
    pthread_mutex_t mtx;
    pthread_mutex_init(&mtx, nullptr);

    srand((unsigned long)time(nullptr) ^ getpid() ^ 433);
    pthread_t t[THREAD_NUM];
    for (int i = 0; i < THREAD_NUM; i++)
    {
        string name = "thread ";
        name += to_string(i + 1);
        threaddata *td = new threaddata(name, &mtx);
        pthread_create(t + i, nullptr, getticket, (void *)td);
    }

    for (int i = 0; i < THREAD_NUM; i++)
        pthread_join(t[i], nullptr);

    pthread_mutex_destroy(&mtx);

    return 0;
}

3、锁的原理

通过加锁,我们能够保证执行临界资源的操作是原子的。可是,访问临界资源时,多个线程要申请同一把锁,那么就必须要能够看到同一把锁,那么这个锁不就成了一个临界资源了吗,那锁是怎么保证自己的安全的呢?

为了保证锁的安全,申请和释放锁的操作也必须是原子的。如何保证呢?

在汇编的角度,如果只有一行汇编语句,我们就认为该汇编语句的执行是原子的。一般来说,是使用swap或exchange指令,以一条汇编语句,将内存和CPU寄存器的数据进行交换。如下图:

线程a是第一个申请锁的。它先将 %al 的内容写成 0,然后交换 %al 和 mutex 的内容,%al 为 1,mutex为0。接着,判断%al的内容 >0,返回,成功拿到锁。线程a切出,寄存器%al的数据作为上下文随线程a一起切出。(当然,线程a可能在任何时候被切出,这是线程a时间片比较长的情况)。

线程b,接着申请锁。 它也先将 %al 的内容写成 0,然后交换 %al 和 mutex 的内容,%al 为 0,mutex为0。接着,判断%al的内容不大于0,于是线程b挂起等待。只有线程a将锁释放后,才能重新申请锁。

4、死锁

死锁:多线程场景中, 多个执行流彼此申请对方的锁资源,并且还不释放自己已申请的锁资源,进而导致执行流无法继续向下执行代码的现象。

产生死锁四个必要条件:
1、互斥条件:一个资源每次只能被一个执行流使用。
2、请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
4、循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

避免产生死锁:
1、破坏死锁的四个必要条件
2、加锁顺序一致
3、避免锁未释放的场景
4、资源一次性分配

三、可重入与线程安全

1、概念

~ 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

~ 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

2、常见的线程不安全的情况

1、不保护共享变量的函数。
2、函数状态随着被调用,状态发生变化的函数。
3、返回指向静态变量指针的函数。
4、调用线程不安全函数的函数。

3、常见不可重入的情况

1、调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
2、调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
3、可重入函数体内使用了静态的数据结构。

4、可重入与线程安全联系

1、函数是可重入的,那就是线程安全的
2、函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
3、如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

5、可重入与线程安全区别

1、可重入函数是线程安全函数的一种
2、线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
3、如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/458421.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

idea 导入项目

idea 导入项目并运行 导入设置设置 jdk查看maven 设置 导入 在项目首页 或者 file 选择 open, 然后选择项目根路径 设置 设置 jdk 查看maven 设置

Linux基础命令[18]-whoami

文章目录 1. whoami 命令说明2. whoami 命令语法3. whoami 命令示例4. 总结 1. whoami 命令说明 whoami&#xff1a;用于显示当前用户名&#xff0c;功能与 id -un 相同。基本信息如下&#xff1a; Usage: whoami [OPTION]... Print the user name associated with the curre…

数码管动态扫描显示

摸鱼记录 Day_16 (&#xff9f;O&#xff9f;) review 前边已经学习了&#xff1a; 串口接收&#xff1a;Vivado 串口接收优化-CSDN博客 1. 今日摸鱼任务 串口接收数据 并用数码管显示 (&#xff9f;O&#xff9f;) 小梅哥视频&#xff1a; 17A 数码管段码显示与动态扫…

ES6(一):let和const、模板字符串、函数默认值、剩余参数、扩展运算符、箭头函数

一、let和const声明变量 1.let没有变量提升&#xff0c;把let放下面打印不出来&#xff0c;放上面可以 <script>console.log(a);let a1;</script> 2.let是一个块级作用域,花括号里面声明的变量外面找不到 <script>console.log(b);if(true){let b1;}//und…

matlab 基操~

MATLAB基本操作 1. 对象定义 使用sym定义单个对象、使用syms定义多个对象 2. 使用limit求极限 $$ \lim_{v \rightarrow a} f(x) $$ limit(f,v,a) % 使用limit(f,v,a,left)可求左极限 3. 导数 使用diff(f,v,n)对$ f(v)v^{t-1} $求 $ n $ 阶导 $ \frac{d^nf}{d^nv} $&#xf…

【蓝桥杯-单片机】基础模块:数码管

文章目录 【蓝桥杯-单片机】基础模块&#xff1a;数码管01 数码管原理图什么是位选和段选共阳极数码管和共阴极数码管的区分&#xff08;1&#xff09;共阳极数码管&#xff08;Common Anode&#xff09;&#xff1a;&#xff08;2&#xff09;共阴极数码管&#xff08;Common …

C语言中大小写字母如何转化

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

【Miniconda】基于conda列出当前环境下所有已创建的虚拟环境

【Miniconda】基于conda列出当前环境下所有已创建的虚拟环境 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希望得到您的…

MS08-067 漏洞利用与安全加固

文章目录 环境说明1 MS08_067 简介2 MS08_067 复现过程3 MS08_067 安全加固 环境说明 渗透机操作系统&#xff1a;2024.1漏洞复现操作系统: Windows XP Professional with Service Pack 2- VL (English)安全加固复现操作系统&#xff1a;Windows XP Professional with Service …

Windows系统搭建Cloudreve结合内网穿透打造可公网访问的私有云盘

目录 ⛳️推荐 1、前言 2、本地网站搭建 2.1 环境使用 2.2 支持组件选择 2.3 网页安装 2.4 测试和使用 2.5 问题解决 3、本地网页发布 3.1 cpolar云端设置 3.2 cpolar本地设置 4、公网访问测试 5、结语 ⛳️推荐 前些天发现了一个巨牛的人工智能学习网站&#xff…

BUGKU-WEB shell

题目描述 题目截图如下&#xff1a; 描述&#xff1a; $poc "a#s#s#e#r#t";$poc_1 explode("#", $poc);$poc_2 $poc_1[0].$poc_1[1].$poc_1[2].$poc_1[3].$poc_1[4].$poc_1[5];$poc_2($_GET[s])进入场景看看&#xff1a;是一个空白的界面 解题思路 …

HTML 学习笔记(十)块和内联

每个HTML元素都有一个默认的显示值&#xff0c;显示值又可以再分为block(块)和inline(内联) 一、块元素 通过F12进入浏览器开发者模式查看该元素会发现其所占宽度为整个网页的宽度 1.div标签 通过div标签将一些元素装进"盒子"&#xff0c;从而对盒子中的全部元素…

OPTIONS请求(跨域预检查)

目录 一、什么是OPTIONS请求&#xff1f;二、简单请求、复杂请求三、特定的请求头、响应头 一、什么是OPTIONS请求&#xff1f; OPTIONS 请求方式是 HTTP 协议中的一种&#xff0c;主要用于 从响应头中获取服务器支持的HTTP请求方式。 OPTIONS 请求方式是 浏览级行为&#xf…

Hubspot 2023年推荐使用的11个AI视频生成器

视频是任何营销活动不可或缺的一部分&#xff1b;然而&#xff0c;如果你不懂编辑或时间紧迫&#xff0c;它们可能会很乏味&#xff0c;很难创建。一只手从电脑里伸出来&#xff0c;拳头碰到另一只手&#xff1b;代表AI视频生成器。 幸运的是&#xff0c;你可以利用许多人工智能…

市场复盘总结 20240314

仅用于记录当天的市场情况&#xff0c;用于统计交易策略的适用情况&#xff0c;以便程序回测 短线核心&#xff1a;不参与任何级别的调整&#xff0c;采用龙空龙模式 一支股票 10%的时候可以操作&#xff0c; 90%的时间适合空仓等待 二进三&#xff1a; 进级率中 25% 最常用的…

vue 引用百度地图

address.vue <template><div><!-- 地图 --><el-drawer:visible.sync"type1"direction"rtl"size"50%"append-to-bodyclass"map-drawer":before-close"beforeClose"><div style"width: 100%…

Twitter广告投放技巧

明确目标受众&#xff1a; 确定你的目标受众&#xff0c;包括他们的兴趣、地理位置、年龄等。Twitter提供了广告定位选项&#xff0c;确保你的广告被展示给最相关的用户。 使用吸引人的图像和视频&#xff1a; 在Twitter上&#xff0c;图像和视频是引起关注的关键。确保你的广…

IAB视频广告标准《数字视频和有线电视广告格式指南》之 简介、目录及视频配套广告 - 我为什么要翻译介绍美国人工智能科技公司IAB系列(2)

写在前面 谈及到中国企业走入国际市场&#xff0c;拓展海外营销渠道的时候&#xff0c;如果单纯依靠一个小公司去国外做广告&#xff0c;拉渠道&#xff0c;找代理公司&#xff0c;从售前到售后&#xff0c;都是非常不现实的。我们可以回想一下40年前&#xff0c;30年前&#x…

吐槽FineDataLink工具Format函数处理日期转字符串格式的说明文档

一.背景 为公司师带徒的任务做些记录。 二.文档存在的问题 1.文档情况 FORMAT-格式转换- FineBI帮助文档 FineBI帮助文档 函数定义&#xff1a; FORMAT(object,format) formart的格式有哪些呢&#xff1f;我们截图看看&#xff1a; 2.文档说明不足问题 同事的需求是把时…

Kubernetes operator系列:webhook 知识学习【更新中】

云原生学习路线导航页&#xff08;持续更新中&#xff09; 本文是 Kubernetes operator学习 系列文章&#xff0c;本节会对 kubernetes webhook 知识进行学习 本文的所有代码&#xff0c;都存储于github代码库&#xff1a;https://github.com/graham924/share-code-operator-st…