[Linux#42][线程] 锁的接口 | 原理 | 封装与运用 | 线程安全

news2025/2/5 22:14:42

 互斥量 mutex

• 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间 内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

• 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通 过数据的共享,完成线程之间的交互。

• 多个线程并发的操作共享变量,会带来一些问题。

例如想进 单人自习室,要拿钥匙,申请的是同一把锁

1. 锁的接口

定义锁

锁也是一个数据类型,它的类型是pthread_mutex_t

初始化

  1. 静态分配
  • pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER(不需要销毁)
  1. 动态分配
  • 函数原型:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数:

  • mutex:要初始化的互斥量
  • attr:初始化互斥量的属性,一般设置为nullptr即可

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

对于锁的五个基本使用:

  • 注意:加锁的时候,一定要保证加锁的粒度,越小越好

实操:

//定义
 pthread_mutex_t mtx;
//初始化
pthread_mutex_init(&mtx, nullptr);
//销毁
pthread_mutex_destroy(&mtx);
//给线程上锁
 int n = pthread_mutex_lock(td->_pmtx);
//解锁
 n = pthread_mutex_unlock(td->_pmtx);

示例场景: 

#define THREAD_NUM 5

int tickets = 10000;

class ThreadData
{
public:
    ThreadData(const string &name, pthread_mutex_t *pm)
    : _name(name)
    , _pmtx(pm)
    {}
public:
    string _name;
    pthread_mutex_t *_pmtx;
};

void *GetTickets(void *args)
{
    ThreadData *td = (ThreadData *)args;
    while(true)
    {
        int n = pthread_mutex_lock(td->_pmtx);
        assert(n == 0);

        if(tickets > 0)
        {
            usleep(1000);
            printf("%s:%d\n", td->_name.c_str(), tickets);
            tickets--;
            n = pthread_mutex_unlock(td->_pmtx);
            assert(n == 0);
        }
        else
        {
            n = pthread_mutex_unlock(td->_pmtx);
            assert(n == 0);
            break;
        }
    }

    delete td;
    return nullptr;
}

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

    // 多线程抢票逻辑
    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, GetTickets, (void *)td);
    }

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

    pthread_mutex_destroy(&mtx);
    return 0;
}
  • 加锁的本质:是用时间来换取安全
  • 加锁的表现:线程对于临界区代码串行执行
  • 加锁原则:尽量的要保证临界区代码,越少越好

加锁和解锁之间称之为 临界区

 int n = pthread_mutex_lock(td->_pmtx);
        assert(n == 0);

        if(tickets > 0)
        {
            usleep(1000);
            printf("%s:%d\n", td->_name.c_str(), tickets);
            tickets--;
            n = pthread_mutex_unlock(td->_pmtx);

申请锁成功了,才能往后执行,不成功,阻塞等待

  • 线程对于锁的竞争能力可能会不同, 刚执行完,离锁近,所以可能会某一线程一直更容易拿到锁
  • 我们抢到了票,我们会立马抢下一张吗?其实多线程还要执行得到票之后的后续动作。usleep模拟
  • 纯互斥环境,如果锁分配不够合理,容易导致其他线程的饥饿问题!不是说只要有互斥,必有饥饿。适合纯互斥的场景,就用互斥

观察员:

  • 外面来的,必须排队
  • 出来的人,并不能立马重新申请锁,必须排队到队尾

让所有的线程(人 )获取锁,按照一定的顺序。

按照一定的顺序获取资源--同步!

  • 锁本身就是共享资源
  • 所以,申请锁和释放锁本身就是原子的

临界区中,线程可以被切换吗?可以切换

  • 在线程被切出去的时候,是持有锁被切走的。我不在期间,照样没有人能进入临界区访问临界资源
  • 对于其他线程来讲,一个线程要么么有锁,要么释放锁
  • 当前线程访问临界区的过程,对于其他线程是原子的

💡引入新的解决方案,也会伴随着新的问题,在于看重什么,对立与统一 锁添加的智慧


2. 锁的原理

加锁

tickets-- 不是原子的?会变成 3 条汇编语句。原子:一条汇编语句就是原子的

  • 为了实现互斥锁操作,大多数体系结构都提供了 swap 或 exchange 指令,该指令 的作用是把存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使 是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另 一个处理器的交换指令只能等待总线周期。 现在我们把 lock 和 unlock 的伪代码改 一下

三部分的刷入,刷出,制作动图如下

(线程加载到寄存器,与内存实现交换后,带着数据及上下文游走)

交换的本质:把内存中的数据(共享),交换到 CPU 的寄存器中,其实是换到 CPU 此时执行的线程的硬件上下文中,数字 1 (锁)只有一个,随着上下文走了

把一个共享的锁,让一个线程以一条汇编的方式,交换到自己的上下文中--当前线程持有锁了

解锁

将 1 还回去,通过 unlock

💡许多奇奇怪怪的问题,是需要程序员你自己来维护的,这就设计到加锁位置的智慧了


3. 锁的应用--封装

锁的设置:(降低耦合度

class Mutex
{
public:
    Mutex(pthread_mutex_t *lock):lock_(lock)
    {}
    void Lock()
    {
        pthread_mutex_lock(lock_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(lock_);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t *lock_;
};

封装:利用初始化来对锁进行启动

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *lock):mutex_(lock)
    {
        mutex_.Lock();
    }
    ~LockGuard()
    {
        mutex_.Unlock();
    }
private:
    Mutex mutex_;
};

调用:

#include "LockGuard.hpp"
while (true)
    {
        {
            LockGuard lockguard(&lock); // 临时的LockGuard对象, RAII风格的锁!
            if (tickets > 0)
            {
                usleep(1000);
                printf("who=%s, get a ticket: %d\n", name, tickets); // ?
                tickets--;
            }
            else
                break;
        }
        usleep(13); // 我们抢到了票,我们会立马抢下一张吗?其实多线程还要执行得到票之后的后续动作。usleep模拟
    }

批注:

  1. LockGuard lockguard(&lock); // 临时的LockGuard对象, RAII风格的锁!

while 之后会自动解锁,利用了对象的生命周期

  1. 第二个{ ,明确出了锁的临界区

4. 可重入 VS 线程安全

概念

  • 线程安全--多线程的并发:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变 量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入--函数的特点同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其 他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会 出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数

可重入的一般也是线程安全的

常见的线程不安全的情况

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

常见的线程安全的情况

• 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般 来说这些线程是安全的

• 类或者接口对于线程来说都是原子操作

• 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

• 调用了 malloc/free 函数,因为 malloc 函数是用全局链表来管理堆的

• 调用了标准 I/O 库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构,例如 STL

• 可重入函数体内使用了静态的数据结构

常见可重入的情况

• 不使用全局变量或静态变量

• 不使用用 malloc 或者 new 开辟出的空间

• 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

• 函数是可重入的,那就是线程安全的

• 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

• 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别

• 可重入函数是线程安全函数的一种

• 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

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

死锁--代码不会再完后推进了,例如:

 while (true)
    {
        pthread_mutex_lock(&lock); // 申请锁成功,才能往后执行,不成功,阻塞等待。
        pthread_mutex_lock(&lock); // 申请锁成功,才能往后执行,不成功,阻塞等待。
        if(tickets > 0)
        {
            usleep(1000);
            ...}
    }

下篇文章将继续讲解~

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录算法训练营第二十一天(二叉树 八)

今天是二叉树复习最后一天&#xff01; 力扣题部分: 669. 修剪二叉搜索树 题目链接:. - 力扣&#xff08;LeetCode&#xff09; 题面: 给你二叉搜索树的根节点 root &#xff0c;同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树&#xff0c;使得所有节点的值在[low…

使用 Dify 和 AI 大模型理解视频内容:Qwen 2 VL 72B

接下来的几篇相关的文章&#xff0c;聊聊使用 Dify 和 AI 大模型理解视频内容。 本篇作为第一篇内容&#xff0c;以昨天出圈的“黑神话悟空制作人采访视频”为例&#xff0c;先来聊聊经常被国外厂商拿来对比的国产模型&#xff1a;千问系列&#xff0c;以及它的内测版。 写在…

Linux非VP扩容方案

Linux系统非VP扩容方案 描述&#xff1a;现有虚拟机磁盘1TB 容量不够&#xff0c;需要扩容。 采用&#xff1a;https://bbs.sangfor.com.cn/forum.php?modviewthread&tid110403 扩容失败。原因是没有VP 和LV 解决方案&#xff1a; 1&#xff0c;查看分区 cat /proc/p…

鸿蒙内核源码分析(中断概念篇) | 海公公的日常工作

关于中断部分系列篇将用三篇详细说明整个过程. 中断概念篇 中断概念很多&#xff0c;比如中断控制器&#xff0c;中断源&#xff0c;中断向量&#xff0c;中断共享&#xff0c;中断处理程序等等.本篇做一次整理.先了解透概念才好理解中断过程.本篇的主角是海公公&#xff0c;用…

全国计算机二级C语言笔试试题及答案

一、选择题(每小题2分,共70分)   下列各题A)、B)、C)、D)四个选项中,只有一个选项是正确的。 请将正确选项填涂在答题卡相应位置上,答在试卷上不得分。   (1)下列叙述中正确的是 A)线性表的链式存储结构与顺序存储结构所需要的存储空间是相同的 …

day06-SpringBootWeb请求响应

前言 在上一次的课程中&#xff0c;我们开发了springbootweb的入门程序。 基于SpringBoot的方式开发一个web应用&#xff0c;浏览器发起请求 /hello 后 &#xff0c;给浏览器返回字符串 “Hello World ~”。 其实呢&#xff0c;是我们在浏览器发起请求&#xff0c;请求了我们的…

[Meachines] [Easy] Bastion SMB未授权访问+VHD虚拟硬盘挂载+注册表获取NTLM哈希+mRemoteNG远程管理工具权限提升

信息收集 IP AddressOpening Ports10.10.10.134TCP:22, 135, 139, 445, 5985, 47001, 49664, 49665, 49666, 49667, 49668, 49669, 49670 $ nmap -p- 10.10.10.134 --min-rate 1000 -sC -sV PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH fo…

Bonree ONE 3.0:全域可观测 运维新境界

2024年8月16日&#xff0c;备受瞩目的Bonree ONE 3.0产品发布会上海站在上海中心大厦隆重举行。此次发布会以”Take IT Easy“——全域可观测&#xff0c;运维新境界为主题&#xff0c;博睿数据正式发布了一体化智能可观测平台Bonree ONE 3.0版本。Bonree ONE 3.0凭借领先的全域…

超声波清洗机哪个品牌好用?值得入手的超声波清洗机品牌推荐

许多人初次使用超声波清洗机的场景&#xff0c;往往发生在眼镜店内。它能灵巧穿梭于眼镜鼻托等细微缝隙间&#xff0c;实现彻底清洁&#xff0c;成效显著。这不仅限于眼镜&#xff0c;各式小件物品同样能享受到这份深度洁净的待遇。尽管超声波清洗机或许并非日常生活中的绝对必…

LeetCode.80.删除有序数组中的重复项II

题目描述&#xff1a; 给你一个有序数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使得出现次数超过两次的元素只出现两次 &#xff0c;返回删除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须在 原地 修改输入数组 并在使用 O(1) 额外空间…

【MySQL-23】万字总结<InnoDB引擎>——【逻辑存储结果&架构(内存结构,磁盘结构,后台线程)&事务原理&MVCC】

前言 大家好吖&#xff0c;欢迎来到 YY 滴MySQL系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的《Lin…

【nginx】详细详细超详细,包括编译安装nginx+升级+回滚+核心配置+高级配置+反向代理+Nginx Rewrite相关功能等等

理论部分&#xff1a; 企业高性能Web服务器Nginx是一个开源的、高性能的HTTP和反向代理服务器&#xff0c;同时也支持IMAP/POP3/SMTP协议。它由俄罗斯人Igor Sysoev开发&#xff0c;并在2004年以BSD-like协议发布。Nginx因其卓越的性能、稳定性、丰富的功能集以及简单的配置而…

连锁美业门店收银系统怎么选?什么样的美业系统好用?美业管理系统源码分享

通过“PCiPAD手机APP微信小程序”的便捷功能操作&#xff0c; 提升预约服务、 会员管理、 收银管理、 库存管理、 客勤维护、员工管理、 排班管理等流程效率&#xff0c; 让门店员工的工作重心回归到服务质量上。 ▲ 小程序 ▲ 手机APP ▲ PC管理后台

Aseembly(九)-[BX] Loop

正如本篇文章的标题所示:本篇文章主要是进行 [BX] 和loop的讲解 上篇文章我们讲述了 关于 自己去dosbox里面编写汇编程序并且一步一步的编译(masm) 链接(link) 然后进行debug的过程 ,也进行了一个关于栈的实验: 详情请见我的上一篇文章 Aseembly(八)-汇编语言编写程序 让我们…

JavaScript class和正则

正则表达式练习 出生日期 年 月 日 ()表示一个整体 console.log(1909.match(^19\\d{2}$)); console.log(2024.match(^20(([01][0-9])|(2[0-4]))$)); //年 console.log(1909.match(^(19\\d{2})|(20(([01][0-9])|(2[0-4])))$)); // 月 console.log(12.match(^(0[1-9])|(1[0-2])…

minio版本升级与数据迁移操作记录

系列文章目录 minio单节点与集群安装 文章目录 系列文章目录前言一、问题引出二、升级与数据迁移步骤0.资源清单1.部署及启动新minio单实例2.设置新旧实例的别名3.检查旧实例bucket及存储的文件4.通过mc客户端命令进行数据迁移5.迁移结果验证 三、新旧实例minio数据对比 前言 …

利用Geohash算法,快速检索周边兴趣点

文章目录 一、前言二、基本原理三、Geohash算法四、算法存在的问题五、代码实现六、问题解决处理 一、前言 需要一个需求&#xff0c;查找某小区附近的超市&#xff0c;如果该小区和超市距离在500米以内&#xff0c;则查找成功。 实现该功能按照传统方式&#xff0c;需要获取小…

【秒杀系统架构图】

文章目录 高并发防止超卖和恶意请求定时同步商品用户秒杀的流程秒杀服务的关注点&#xff1a; 秒杀系统一般出现正在电商平台中&#xff0c;秒杀系统需要支持高并发&#xff0c;保持一致性和高可用的特点&#xff1a; 高性能。 秒杀涉及大量的并发读和并发写&#xff0c;因此支…

QT:事件机制

一、事件机制 qt的核心机制&#xff1a;对象树、信号和槽、事件机制 1.1概念 就是当这件事情发生时&#xff0c;自动执行对应的功能代码。该某块功能代码是虚函数&#xff0c;只需重写该虚函数&#xff0c;即可执行重写的代码。 1.2事件处理简介 1. 什么是事件&#xff1f; (重…

algorithm算法库学习之——堆操作,最小/最大操作,比较操作,排列操作

algorithm此头文件是算法库的一部分。本篇介绍堆操作&#xff0c;最小/最大操作&#xff0c;比较操作&#xff0c;排列操作。 接口API 堆操作 is_heap 检查给定范围是否为一个最大堆 (函数模板) is_heap_until (C11) 查找能成为最大堆的最大子范围 (函数模板) make_heap 从一…