Linux多线程系列2: 模拟封装简易语言级线程库,线程互斥和锁,线程同步和条件变量,线程其他知识点

news2024/11/19 17:26:53

Linux多线程系列2: 模拟封装简易语言级线程库,线程互斥和互斥锁,线程同步和条件变量,线程其他知识点

    • 1.前言
  • 一.模拟C++11线程库自己封装简易语言级线程库
    • 1.实现框架
    • 2.迅速把构造等等函数写完
    • 3.start和work
      • 1.尝试一
      • 2.尝试二
      • 3.最终版本
      • 4.给出代码
  • 二.模拟实现多线程(为编写线程池做准备)
    • 1.进程池部分代码修改一下
    • 2.代码走起
  • 三.线程互斥与互斥锁
    • 1.模拟多线程抢票的场景
      • 1.代码
    • 2.原子性
      • 1.介绍
      • 2.分析一下问题原因
    • 3.介绍并使用互斥锁解决问题
      • 1.介绍
      • 2.法一:全局锁
      • 互斥锁导致的线程饥饿问题
      • 3.法二
      • 4.RAII登场
        • 1.引入
        • 2.代码
      • 5.小小总结
    • 4.互斥锁的原理
      • 1.说明
      • 2.演示
      • 3.小总结
      • 4.申请释放锁一定不会陷入内核吗?
    • 5.频繁申请释放锁导致程序变慢的原因
  • 四.线程同步与条件变量
    • 1.线程同步与场景的引入
    • 2.代码与演示
    • 3.条件变量的理解
    • 4.接口
    • 5.signal使用
    • 6.伪唤醒结论
    • 7.伪唤醒演示
      • 1.broadcast导致伪唤醒
      • 2.signal导致伪唤醒
      • 3.解决伪唤醒的bug
        • 1.broadcast
        • 2.signal
  • 五.线程安全与可重入
    • 1.线程安全
    • 2.可重入
    • 3.两者的区别与联系
  • 六.死锁
    • 1.概念
    • 2.四个必要条件
    • 3.如何预防/解决死锁问题
  • 七.读者写者模型(读写锁)
    • 1.介绍
    • 2.接口
    • 3.原理
  • 八.自旋锁
    • 1.什么是自旋锁
    • 2.为何要有自旋锁/自旋锁的应用场景

1.前言

我们之前简单了解了C++11线程库的一部分,今天我们试着写一下C++11的线程库,并且模拟实现一下多线程(今天就是代码环节),写完一堆代码之后,我们进入线程互斥,锁,线程安全和可重入部分的学习,依旧是代码+理论

当然绝对没有写库的大佬们写的那么完整且周到,我们实现的版本能让我们很好的使用即可,我们模拟实现主要是为了

  1. 为了后续实现线程池做准备(我们以后的线程池就用这个我们自己写的线程库了,因为我们自己写的用起来更随心所欲,库里的只能按大佬设计的走
  2. 让我们更加熟悉线程与线程接口,更加适应多线程环境代码的编写
  3. 增强代码能力
  4. 体会编程的乐趣(写了一年多单执行流的代码[除了进程池和进程间通信让我们体会到了多执行流的乐趣]了,该换换口味了)

废话不多说,直接走起

一.模拟C++11线程库自己封装简易语言级线程库

在这里插入图片描述

1.实现框架

在这里插入图片描述
什么代码都是这样,有了框架,下面就是实现函数,并按需求更新框架的工作了

2.迅速把构造等等函数写完

代码:
在这里插入图片描述
验证
在这里插入图片描述
没任何问题

3.start和work

1.尝试一

在这里插入图片描述

2.尝试二

我们把work加上static修饰
在这里插入图片描述

3.最终版本

在这里插入图片描述

4.给出代码

#pragma once
#include <iostream>
using namespace std;
#include <string>
#include <functional>
#include <pthread.h>
//T是函数对象的参数类型
template<class T>
class Thread
{
public:
    //创建线程对象,初始化成员变量
    Thread(const string& name,const function<void(T)>& func,const T& data)
        :_threadName(name),_func(func),_data(data)
    {
        #ifdef DEBUG//用一下条件编译
        cout<<"构造函数执行完毕: name: "<<_threadName<<endl;
        #endif
    }

    //封装_func为void*(*pf)(void*)的函数对象,因为pthread_create只能传入这个类型的函数对象
    static void* work(void* arg)//arg给我this!!!!,别给错了
    {
        Thread<T>* mythis=static_cast<Thread<T>*>(arg);
        //虽然我是静态成员函数,但是我有this,我想怎么玩就怎么玩
        mythis->_func(mythis->_data);
        return nullptr;
    }
    
    bool start()//启动线程
    {
        if(_isRunning==false)
        {
            pthread_create(&_tid,nullptr,work,this);//没问题!!
            _isRunning=true;
            return true;
        }
        //如果线程已经被启动,就不能再被启动了
        return false;
    }

    bool joinable() const
    {
        return _isJoin;
    }

    bool running() const
    {
        return _isRunning;
    }

    bool join()//回收线程
    {
        if(!_isRunning) cout<<"该线程 "<<_threadName<<" 还未开始"<<endl;
        else if(!_isJoin) cout<<"该线程 "<<_threadName<<" 不可join"<<endl;
        else
            return pthread_join(_tid,nullptr)==0;//返回join是否成功
        return false;//还未开始或者不可join,返回false
    }

    bool detach()//分离线程
    {
        if(!_isRunning) cout<<"该线程 "<<_threadName<<" 还未开始"<<endl;
        else if(pthread_detach(_tid)==0)//如果分离成功
        {
            _isJoin=false;
            return true;
        }
        return false;//分离失败
    }

private:
    string _threadName;//线程名字
    function<void(T)> _func;//具体的函数对象
    T _data;//函数参数
    pthread_t _tid;//线程ID
    bool _isRunning=false;//该线程是否正在运行
    bool _isJoin=true;//该线程可分离
};

二.模拟实现多线程(为编写线程池做准备)

写完我们的线程库之后,我们玩一下特别小的线程池雏形,跟我们进程池的需求一样
此时,我们拿出进程池的User代码过来,改一下

1.进程池部分代码修改一下

User.h
#pragma once
const int task_num=5;
void printLog(int i);
void ConnectDatabase(int i);
void UserLogin(int i);
void GenerateReports(int i);
void TestSoftwarePerformance(int i);
User.cpp
#include <unistd.h>
#include <iostream>
#include <vector>
#include <string>
#include <chrono>  
#include <iomanip>
#include <sstream>
#include "User.h"
using namespace std;
// 打印日志的函数  
void printLog(int i) 
{  
    #ifdef DEBUG
    cout<<"void printLog(int i) i: "<<i<<endl;
    #endif
    // 获取当前时间  
    auto now = std::chrono::system_clock::now();  
    auto now_c = std::chrono::system_clock::to_time_t(now);  
    // 格式化时间戳  
    ostringstream oss;  
    oss << put_time(std::localtime(&now_c), "%Y-%m-%d %H:%M:%S");  
    string timestamp = oss.str();  
    const vector<string>& message={"This is a log message.","Another log message with some information."};
    // 打印日志信息
    for(auto& e:message)
    {
        cout << "[" << timestamp << "] " << e << endl;
    }
    usleep(1000);//休息1000微秒,也就是1ms
}
//下面的我就随便遍了,MySQL还没学....
//连接数据库
void ConnectDatabase(int i)
{
    #ifdef DEBUG
    cout<<"void ConnectDatabase(int i) i: "<<i<<endl;
    #endif
    cout<<"Connect to the database succeed"<<endl;
    usleep(1000);//休息1000微秒,也就是1ms
}

//用户登录
void UserLogin(int i)
{
    #ifdef DEBUG
    cout<<"void UserLogin(int i) i: "<<i<<endl;
    #endif
    cout<<"User login succeed"<<endl;
    usleep(1000);//休息1000微秒,也就是1ms
}

//报告生成
void GenerateReports(int i)
{
    #ifdef DEBUG
    cout<<"void GenerateReports(int i) i: "<<i<<endl;
    #endif
    cout<<"Generate reports succeed"<<endl;
    usleep(1000);//休息1000微秒,也就是1ms
}

//测试软件性能
void TestSoftwarePerformance(int i)
{
    #ifdef DEBUG
    cout<<"void TestSoftwarePerformance(int i) i: "<<i<<endl;
    #endif
    cout<<"Test software performance succeed"<<endl;
    usleep(1000);//休息1000微秒,也就是1ms
}

2.代码走起

thread.hpp不用改
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
关于其他的对于多线程的玩法,有时间了我们再来玩吧,其实锁也挺好玩的,下面进入线程互斥与锁

三.线程互斥与互斥锁

记得我们之前介绍过的信号量吗?
我们当时说二元信号量就是一把互斥锁,并且我们用二元信号量实现了共享内存的互斥,下面我们就来学一下互斥锁和原子性啦
最后的时候我们会引出互斥的局限,此时就需要有不同来解决这一问题

1.模拟多线程抢票的场景

1.代码

这里直接给出代码,然后说明一下,这代码写起来没啥难的
在这里插入图片描述
理想状态: 最后一次ticket打印时是1,然后所有线程都退出
在这里插入图片描述
我们可以看到,整个运行逻辑是非常快的,这就是多线程的一大好处
在这里插入图片描述
记住: 此时我们没有加锁,整个代码运行很快,而且多个线程之间切换的频率也很快(并发性高)
但是:
在这里插入图片描述
我们先介绍一下原子性

2.原子性

1.介绍

在这里插入图片描述
在这里插入图片描述

我们目前的理解是: 只要一条代码不是一条汇编指令,那么这条代码就不是原子的

我们可以类比一下:

if(ticket > 0)这个逻辑判断也不是原子的
为何呢?
1. 将ticket在内存当中的值读取到xxx寄存器当中
2. 将xxx寄存器当中的值与0进行比较,设置条件标志
3. 根据比较结果进行跳转执行if语句或者else语句
至少是有着3步的

2.分析一下问题原因

下面我们利用原子性来分析一下ticket出现0,-1,-2的原因

首先我们要达成共识的是: 
线程在被CPU调度时,只要时间片到了,那么这个线程一定会被切换走,无论即将执行哪条指令
(不考虑线程/进程陷入内核态,因为那个时候OS会进行介入)

在这里插入图片描述
下面我们分析一下
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
只要出现数据不一致,那么就一定会有问题发生(尽管问题发生的概率较小,但是在如此高并发的调度下,次数变多,概率在小的事件发生的次数也会变大啊)

而我们刚才的多线程调度频率算是很快很平均的了
一直都是0 1 2 3 0 1 2 3这样调度
在这里插入图片描述
但是当线程A刚if判断成功进入if语句准备ticket–时,此时时间片到了,需要切换
而且此时ticket就是1,本来这个票应该是我的,因为我这个线程正准备减它,但是我被切换了

然后线程B被调度了,将ticket减为0,然后大部分线程判断的时候看到了ticket都变成0了,我们走else就退出吧
但是当线程A再次被调度时,它是直接执行上次被切换时刚进入的if语句,然后将ticket减为了-1 !!,
此时就导致卖出了比10000张更多的票,导致了很严重的问题

那怎么办呢?
需要利用互斥锁来解决这一问题

3.介绍并使用互斥锁解决问题

注意:ubuntu下man手册没有按照POSIX标准的文档,因此man手册查不到pthread_mutex_destroy
解决方法:sudo apt-get install manpages-posix-dev

mutex:就是互斥锁的意思

1.介绍

man 2 pthread_mutex_destroy
在这里插入图片描述
2.申请锁和释放锁
在这里插入图片描述

2.法一:全局锁

在这里插入图片描述
下面我们演示一下
在这里插入图片描述
在这里插入图片描述
ticket没有出现0,-1,-2,到1就结束了,可见锁成功保护了临界资源
而且: 我们明显发现: 1.运行速度变慢 2.多线程并发效率降低
在这里插入图片描述

互斥锁导致的线程饥饿问题

锁还有其他缺点:
如果某个线程申请锁的能力太强,一直频繁申请锁,释放锁,申请锁,释放锁,就会导致其他线程出现"饥饿"问题!!

我们模拟一下: 某个线程申请锁之后再也不释放与申请这个锁了,直到把票抢光之后在释放锁
在这里插入图片描述
在这里插入图片描述
只有0号线程一直在运行,其他线程都阻塞了

请注意:即使只有一个线程能够抢票,但是多线程的速度依然跟刚才加锁的情况一样

为何??

因为所有线程瓜分进程的时间片,某个线程时间片到了,就会轮到其他线程运行,而其它线程一直阻塞,永远无法抢票(饥饿)

刚才这种情况下倒也不会造成太大的问题: 至少你这个线程把任务都完成了嘛(而且你们这些线程完成的都是同一个任务)
但是如果极端情况下,你这个拥有锁的线程一直不干正事呢?? 比如陷入了某种意外的死循环等等…
或者你们这些线程完成不同任务,但是依然需要申请同一把锁(我总觉得这种情况应该不多吧…)

那就是一个大事故了,因为你这个线程不仅出了错误不干正事,你还导致其他线程饥饿,让其他线程也干不了正事

整个多线程环境就都被这个线程坑了,怎么解决??
利用线程同步(我们以后会重点介绍的,这里先埋一个伏笔,到时候就有场景可以玩了)

注意: 如果某个线程申请锁之后,没有释放锁,并且又申请锁了,那么这个线程就卡死了,这就是一个锁造成的最经典的死锁问题
死锁的时候我们会演示的

3.法二

在这里插入图片描述
在这里插入图片描述
验证成功

4.RAII登场

1.引入

总感觉刚才不是很优雅,而且万一写错了.....................
经过观察,我们发现了一个惊天动地的特点,从而引发了下面…
在这里插入图片描述

2.代码

在这里插入图片描述
演示:
在这里插入图片描述

5.小小总结

在这里插入图片描述
在这里插入图片描述

4.互斥锁的原理

下面我们来谈一下互斥锁的原理,互斥锁是如何保证原子性的(特别是申请锁的原子性),当然,互斥锁有很多实现方式
我们介绍的只是其中一种而已,但是大家理解一下这种保证原子性的方式即可

1.说明

首先要先说明的是:
在介绍进程切换的时候,我们曾经提到过,每个进程(现在是线程)都有自己独立的硬件上下文,而硬件上下文就是该线程被调度时需要存放在寄存器当中的内容

因此我们当时说单核CPU下寄存器只有一套,而寄存器当中的数据是有多套的,每个线程都私有一套寄存器当中的数据,因此才实现了线程切换
在这里插入图片描述
unlock就是把1写入mutex,表示该锁资源重新拥有了
因为每个线程在访问临界资源之前都要先申请锁,而申请锁时都会先把al寄存器当中的内容清为0,所以无需担心释放锁时没有修改持有锁时al寄存器当中的内容

2.演示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
大家可以画图自己感受一下:

  1. xchgb的作用: 本质上就是将锁资源转移到线程自己的硬件上下文当中为该线程所私有
  2. 线程在执行完xchgb之后,谁的al当中是1,谁就有锁资源,否则就没有锁资源

3.小总结

可以看出这个互斥锁的设计是非常巧妙的
通过

  1. 只为锁设置一个1表示该锁的资源
  2. 每个线程申请锁时都先将自己的al寄存器当中的data清0,表示当前该线程没有锁资源
  3. 申请锁时,每个线程都将自己al寄存器当中的data跟mutex交换
  4. 因为整个环境下只有一个1,而且交换的指令是原子的(由计算机的体系结构提供的swap/exchange指令保证)
  5. 因此一定只有一个线程交换之后能拿到1,表示该线程申请锁成功
  6. 而其它线程交换之后只能拿到0,表示该线程申请锁失败
    =============================================================================================
    这一切的一切发生时,互斥锁还都只是一个内存当中用户级/进程级(因为进程是用户的代表嘛)的数据而已
    锁的设计巧妙归巧妙,但是锁终归是会将多线程执行加锁的代码由并发访问改成并行访问,从而降低并发效率
    但是互斥(锁)与同步在生产者消费者模型下就大有裨益了,到那时我们写代码再好好分析一下大佬设计的优雅之处

所以关于锁,希望大家辩证判断,仔细权衡自己的需求之后在妥善适用它们(临界资源的保护? 生产者消费者模型下的应用? …)

不过单拿出锁来说,它本身的确是无奈之举(因为要保护临界资源嘛),如果不加锁就能保证临界资源的安全,那谁还加锁…

4.申请释放锁一定不会陷入内核吗?

你给我说完了互斥锁的原理之后,你强调了mutex是用户空间的变量,而且pthread库也是用户空间的,
在这里插入图片描述

5.频繁申请释放锁导致程序变慢的原因

  1. 加锁代码块的串行化: 多线程执行临界区代码,加锁之后: 由并发执行变为串行执行,限制了多线程的高并发的优点
  2. 互斥锁的锁竞争: 导致多线程频繁陷入内核,回到用户态,陷入内核,回到用户态… 开销较大
  3. CPU缓存的无效化: 如果被锁保护的资源被频繁访问修改,会导致CPU缓存的无效化和重新加载,而CPU缓存的加载才是线程切换优于进程切换的最直接原因,因此这会限制多线程切换代价低的优点

又因为线程是瓜分进程时间片的,因此线程切换的频率更高,而加锁限制了多线程的很多优点,极端情况下会使得多线程还不如单线程不加锁呢…

四.线程同步与条件变量

经过刚才抢票的代码,我们看到了线程互斥完美的保护了临界资源的安全,但是由于多线程竞争锁的能力不同,
抢到锁并执行临界区的线程也就没有任何的顺序性
因此在某些情况下会导致线程饥饿,甚至引发更大的问题

此时就需要有线程同步来保证临界资源访问的顺序性和高效性(高效性大家可能不太好理解,但是我们写代码分析的时候大家就能够很好地理解了)

1.线程同步与场景的引入

在临界资源使用安全的前提下,让多线程访问临界资源具有一定的顺序性就叫做线程同步

在这里插入图片描述
其实不仅仅条件变量可以完成这一任务,信号量也可以,我们以后会使用的,我们以后会写一个信号量+环形队列实现生产者消费者模型的代码

下面我们改造一下抢票的需求:
票初始时只有1000张,每隔一段时间就会多发1000张
但是时间间隔有点长,导致新线程抢光票都退出了,票才补上,因此我们的else 当中就不能break了
所以我们让它打印"没票了…"

为了方便演示,我们用一下全局的互斥锁

2.代码与演示

在这里插入图片描述
在这里插入图片描述
下面我们介绍一下条件变量

3.条件变量的理解

记住锁🔒,条件变量🔔
在这里插入图片描述

4.接口

在这里插入图片描述
在这里插入图片描述

5.signal使用

我们这里就只用一下signal吧,因为broadcast在我们这个场景当中太容易导致伪唤醒了
而且为了方便演示,我们只给100张票,每1s加100张票
而且每次抢票只打印一次票数
在这里插入图片描述
在这里插入图片描述

6.伪唤醒结论

伪唤醒是指线程在等待条件满足时被唤醒,但是因为竞争锁失败,导致临界资源被其他线程所修改从而又使得等待条件不满足,
但是因为对应线程被唤醒之后没有继续检查临界资源是否符合条件而直接访问临界资源导致的一种bug

可能有点长且抽象,我们举一个例子
在这里插入图片描述

7.伪唤醒演示

1.broadcast导致伪唤醒

在这里插入图片描述
在这里插入图片描述
broadcast比起signal来说是一种类似于用户级文件缓冲区似的,将本来要多次陷入内核的操作减为了一次…
所以减少了线程陷入内核的次数,从而提高效率(因为每次唤醒线程都要将进入了阻塞状态的线程再次调度运行起来,势必需要陷入内核执行该操作)
因此一次唤醒所有线程比起一次唤醒一个线程,唤醒多次来说是更加高效的

因此signal适用于需要严格控制线程访问临界资源顺序的场景
而broadcast适用于无需严格控制线程访问临界资源顺序的场景

2.signal导致伪唤醒

在这里插入图片描述

3.解决伪唤醒的bug

将if改成while,让伪唤醒的线程看到自己不满足条件之后继续乖乖的到等待队列当中等

1.broadcast

在这里插入图片描述

2.signal

在这里插入图片描述

五.线程安全与可重入

1.线程安全

线程安全是指:多线程并发访问临界资源时,不会出现临界资源的数据不一致问题

常见的线程安全的情况:

  1. 每个线程对临界资源只读取,不修改
  2. 执行具有原子性的代码(比如定义变量)
  3. 多个线程之间的切换不会导致对应代码的执行结果存在二义性
  4. 加锁/使用信号量了

2.可重入

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

常见不可重入的情况:

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

常见可重入的情况:

  1. 不使用全局变量或静态变量
  2. 不使用用malloc或者new开辟出的空间
  3. 不调用不可重入函数
  4. 不返回静态或全局数据,所有数据都有函数的调用者提供
  5. 使用本地数据,或者通过对全局数据进行本地拷贝来保护全局数据

3.两者的区别与联系

  1. 可重入函数是线程安全函数的一种
  2. 线程安全不一定是可重入的,而可重入函数则一定是线程安全的
  3. 可重入描述的是函数的特点,而线程安全描述的是多线程访问临界资源的特点

六.死锁

1.概念

多执行流在不释放自己所占有资源的情况下互相申请对方所占有的资源而处于的一种永久等待的状态

2.四个必要条件

  1. 互斥 : 即对应资源每次只能被一个执行流所使用
  2. 请求与保持 : 一个执行流在因请求资源而阻塞时,对已持有的资源保持不放
  3. 不剥夺 : 一个执行流已经获得的资源,在使用完毕之前不能被剥夺
  4. 循环等待条件 : 若干执行流之间形成的一种头尾相连的循环等待资源的关系

3.如何预防/解决死锁问题

解决死锁问题 : 破坏四个必要条件当中的任意一个即可
预防死锁问题:

  1. 申请资源时按序申请(加锁顺序一致)
  2. 及时释放锁
  3. 资源一次性分配

七.读者写者模型(读写锁)

在多线程环境当中,有一种情况非常常见:
公共资源被修改的场景较少,被读取的场景较多,读取公共资源并不会修改公共资源
此时这种场景被称为读者写者模型

它的特点是:

读者多,写者少,读场景较多,读数据不修改公共资源

1.介绍

1个交易场所(用特定的容器/数据结构申请的内存空间)
两种角色(读者和写者)
三种关系
读读之间: 不互斥(并发)
读写之间: 互斥+同步
写写之间: 互斥

  1. 为何读读之间不互斥呢?
    因为读者只会读数据,不会把数据给取走
  2. 如果读者一直存在,那么写者就会一直阻塞,也就是读者优先级高(默认情况)
    当然自己使用接口时,也可以自己调整为写者优先级高

写独占,读共享,读锁优先级高

2.接口

在这里插入图片描述
这个不是重点,大家感兴趣的话自己用一下测试测试

3.原理

读者之间不是并发吗? 读者之间又不互斥,为什么有读锁? 只搞一个写锁不就行了吗?
因为读写之间是互斥的,而且读者优先,写者要等到读者都退出之后才能进行写操作

必须为读者要维护一个计数,表示读者的个数,每个读者要进行读操作之前要对计数++,读操作结束之后要对计数–
而这个计数对于读者来说是共享资源,而且读者都要修改该共享资源,而且++和–操作不是原子的,因此读者修改共享资源就要加锁,因此读者也要有锁

用伪代码来解释:
在这里插入图片描述

八.自旋锁

1.什么是自旋锁

当申请锁失败之后,互斥锁会阻塞等待,当锁释放之后会重新唤醒竞争锁

而自旋锁在申请锁失败之后会继续申请锁(被称为自旋/忙等待),直到申请成功为止

2.为何要有自旋锁/自旋锁的应用场景

当执行流访问修改临界资源的速度较快时(比如我们上面的多线程抢票,就只有一个判断和一个–),适合使用自旋锁
速度较慢时,适合使用互斥锁,避免频繁无谓的申请锁操作

因为互斥锁的锁竞争会导致多线程频繁陷入内核,回到用户态,陷入内核,回到用户态(因为要不断切换状态,一会被OS放到运行队列,一会又被OS放到阻塞队列)… 开销较大

而自旋锁无需进行陷入内核,回到用户态的操作,因为锁是进程地址空间当中用户空间的一个内存级变量(我们介绍互斥锁原理的时候说的)

以上就是Linux多线程系列2: 模拟封装简易语言级线程库,线程互斥和互斥锁,线程同步和条件变量,线程其他知识点的全部内容,希望能对大家有所帮助!!

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

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

相关文章

互联网医院开发:引领智慧医疗新时代

随着科技的迅猛发展和互联网的普及&#xff0c;传统医疗模式正在迎来一场深刻的变革。互联网医院的崛起&#xff0c;打破了时间和空间的限制&#xff0c;为患者和医疗机构带来了更加便捷、高效、安全的医疗服务体验。本文将从技术角度深入探讨互联网医院的开发&#xff0c;包括…

Hotcoin Research | 市场洞察:2024年5月13日-5月19日

加密货币市场表现 目前&#xff0c;加密货币总市值为1.32万亿&#xff0c;BTC占比54.41%。 本周行情呈现震荡上行的态势&#xff0c;BTC在5月15日-16日&#xff0c;有一波大的拉升&#xff0c;周末为震荡行情。BTC现价为67125美元。 上涨的主要原因&#xff1a;美国4月CPI为3…

安防监控视频平台EasyNVR级联视频上云系统EasyNVS出现“Login error”报错的原因排查

EasyNVR安防视频云平台是旭帆科技TSINGSEE青犀旗下支持RTSP/Onvif协议接入的安防监控流媒体视频云平台。平台具备视频实时监控直播、云端录像、云存储、录像检索与回看、告警等视频能力&#xff0c;能对接入的视频流进行处理与多端分发&#xff0c;包括RTSP、RTMP、HTTP-FLV、W…

RNN-循环神经网络

目录 词嵌入层 RNN网络层 RNN 网络原理 这一节我们学习循环神经网络&#xff0c;RNN~~ 自然语言处理&#xff08;Nature language Processing, NLP&#xff09;研究的主要是通过计算机算法来理解自然语言。对于自然语言来说&#xff0c;处理的数据主要就是人类的语言&#…

安卓绕过限制直接使用Android/data无需授权,支持安卓14(部分)

大家都知道&#xff0c;安卓每次更新都会给权限划分的更细、收的更紧。   早在安卓11的时候还可以直接通过授权Android/data来实现操作其他软件的目录&#xff0c;没有之前安卓11授权的图了&#xff0c;反正都长一个样&#xff0c;就直接贴新图了。   后面到了安卓12~13的…

基于语音识别的智能电子病历(三)之 M*Modal

讨论“基于语音识别的智能电子病历”&#xff0c;就绕不开 Nuance 和 M*Modal。这2个公司长时间的占据第一和第二的位置。下面介绍一下M*Modal。 这是2019年的一个新闻“专业医疗软件提供商3M公司为自己购买了一份圣诞礼物&#xff0c;即M*Modal IP LLC的医疗技术业务&#xf…

Github上传时报错The file path is empty的解决办法

问题截图 文件夹明明不是空的&#xff0c;却怎么都上传不上去。 解决方案&#xff1a; 打开隐藏文件的开关&#xff0c;删除原作者的.git文件 如图所示&#xff1a; 上传成功&#xff01;

溪谷联运SDK功能全面解析

近期&#xff0c;备受用户关注的手游联运10.0.0版本上线了&#xff0c;不少用户也选择了版本更新&#xff0c;其中也再次迎来了SDK的更新。溪谷软件和大家一起盘点一下溪谷SDK的功能都有哪些吧。 一、溪谷SDK具有完整的运营功能和高度扩展性 1.登录&#xff1a;登录是SDK最基础…

洗地机哪个牌子好?全面评测多款口碑洗地机

洗地机的出现&#xff0c;让人们摆脱了每天打扫卫生的繁琐&#xff0c;因为它只需轻轻一推&#xff0c;就能把扫地、拖地、擦地的活全做了&#xff0c;干垃圾湿垃圾统统都能一次清理干净&#xff0c;操作简单&#xff0c;更轻松。本文主要分享一些挑选洗地机的技巧&#xff0c;…

创建vue工程、Vue项目的目录结构、Vue项目-启动、API风格

环境准备 介绍&#xff1a;create-vue是Vue官方提供的最新的脚手架工具&#xff0c;用于快速生成一个工程化的Vue项目create-vue提供如下功能&#xff1a; 统一的目录结构 本地调试 热部署 单元测试 集成打包依赖环境&#xff1a;NodeJS 安装NodeJS 一、 创建vue工程 npm 类…

物体检测算法-R-CNN,SSD,YOLO

物体检测算法-R-CNN&#xff0c;SSD&#xff0c;YOLO 1 R-CNN2 SSD3 Yolo总结 1 R-CNN R-CNN&#xff08;Region-based Convolutional Neural Network&#xff09;是一种基于区域的卷积神经网络&#xff0c;是第一个成功将深度学习应用到目标检测上的算法。它主要由三个步骤组…

2024最新 Jenkins + Docker 实战教程(四) - 编写自己的Springboot项目实现自动化部署

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

v-md-editor和SSE实现ChatGPT的打字机式输出

概述 不论是GPT还是文心一言&#xff0c;在回答的时候类似于打字机式的将答案呈现给我们&#xff0c;这样的交互一方面比较友好&#xff0c;另一方面&#xff0c;当答案比较多、生成比较慢的时候也能争取一些答案的生成时间。本文后端使用express和stream&#xff0c;使用SSE将…

一文读懂Linux

前言 为了便于理解&#xff0c;本文从常用操作和概念开始讲起。虽然已经尽量做到简化&#xff0c;但是涉及到的内容还是有点多。在面试中&#xff0c;Linux 知识点相对于网络和操作系统等知识点而言不是那么重要&#xff0c;只需要重点掌握一些原理和命令即可。为了方便大家准…

【BSP开发经验】用户态栈回溯技术

前言 在内核中有一个非常好用的函数dump_stack, 该函数在我们调试内核的过程中可以打印出函数调用关系&#xff0c;该函数可以帮助我们进行内核调试&#xff0c;以及让我们了解内核的调用关系。同时当内核发生崩溃的时候就会自己将自己的调用栈输出到串口。 栈回溯非常有利于我…

Java基础(三)- 多线程、网络通信、单元测试、反射、注解、动态代理

多线程基础 线程&#xff1a;一个程序内部的一条执行流程&#xff0c;只有一条执行流程就是单线程 java.lang.Thread代表线程 主线程退出&#xff0c;子线程存在&#xff0c;进程不会退出 可以使用jconsole查看 创建线程 有多个方法可以创建线程 继承Thread类 优点&#x…

金丝雀发布(灰度发布)介绍 及 声明式管理方法简介

目录 一 应用发布策略 1&#xff0c;滚动发布&#xff08;k8s默认&#xff09; 2&#xff0c;蓝绿发布 3&#xff0c;金丝雀发布 二 金丝雀发布&#xff08;Canary Release&#xff09; &#xff08;灰度发布&#xff09; 1&#xff0c;金丝雀发布图解 2&#xff0…

LeetCode700二叉搜索树中的搜索

题目描述 给定二叉搜索树&#xff08;BST&#xff09;的根节点 root 和一个整数值 val。你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在&#xff0c;则返回 null 。 解析 最基本的二叉搜索树的应用&#xff0c;递归或者while循环都可以…

PostgreSQL 连接和管理问题解决方案

在使用PostgreSQL数据库时&#xff0c;可能会遇到一些连接和管理方面的问题。本文将详细介绍如何解决“Peer authentication failed”和“password authentication failed”错误&#xff0c;并提供卸载PostgreSQL的方法。 问题一&#xff1a;Peer Authentication Failed Peer …

2024最新彩虹聚合DNS管理系统源码v1.3 全开源

简介&#xff1a; 2024最新彩虹聚合DNS管理系统源码v1.3 全开源 聚合DNS管理系统可以实现在一个网站内管理多个平台的域名解析&#xff0c;目前已支持的域名平台有&#xff1a;阿里云、腾讯云、华为云、西部数码、DNSLA、CloudFlare。 本系统支持多用户&#xff0c;每个用户可…