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

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: 模拟封装简易语言级线程库,线程互斥和互斥锁,线程同步和条件变量,线程其他知识点的全部内容,希望能对大家有所帮助!!

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

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

相关文章

Llama 3超级课堂作业笔记

文章目录 基础作业完成 Llama 3 Web Demo 部署环境配置下载模型Web Demo 部署对话截图 使用 XTuner 完成小助手认知微调Web Demo 部署自我认知训练数据集准备训练模型推理验证 使用 LMDeploy 成功部署 Llama 3 模型环境&#xff0c;模型准备LMDeploy CLI chatLMDeploy模型量化(…

php 连接sqlserver步骤

1.首先要确定使用的是sqlserver的哪个版本&#xff0c;比如sqlserver2012 2.确定服务器是64位还是32位的 3.确认一下使用php的哪个版本&#xff0c;比如php7.1 SQL Server 的 Microsoft PHP 驱动程序 Microsoft Drivers for PHP 支持矩阵 - PHP drivers for SQL Server | Mi…

【传知代码】从零开始搭建图像去雾神经网络-论文复现

文章目录 概述原理介绍网络结构 核心逻辑迁移学习子网数据拟合子网 环境配置训练本次复现代码所用数据集测试本次复现代码所用的评价指标 结果展示在O-Haze数据集上的结果在I-Haze数据集上的结果 小结 本文涉及的源码可从从零开始搭建图像去雾神经网络该文章下方附件获取 本文复…

SAP Credit Management-Reconcile Documented Credit Decisions

ECC 升级S4后&#xff0c;经过事后迁移后&#xff0c;出现如下报错 找到了这里的配置路径&#xff1a; DCD settings – to maintain/complete Create Profile for Case Search -> no entry UKM_CASE – DCD search -> no search fields BC set UKM_DCD_CUST not activa…

open drain 与 push pull

Open drain: open drain 输出&#xff1a;输出端相当于三极管的集电极&#xff0c;要得到高电平需要上拉电阻才行。 栅极输入为0时&#xff0c;NMOS 的漏极和源极导通&#xff0c;输出为0。即Uce 0 V。 栅极输入为1时&#xff0c;NMOS不导通&#xff0c;漏极高祖&#xff0…

Element组件 el-select设置滚动条+滚动加载(两种写法: 原生Js和自定义指令)

系列文章目录 提示&#xff1a;下面是简单的功能实现(时间紧迫,大晚上赶工) el-select滚动条相关功能 系列文章目录背景与展望一、使用原生的js实现触底加载1.效果图如下&#xff1a;2.HTML如下3.JS代码如下 二、使用自定义指令解决问题(已补全-2024-05-19)1.效果图如下:2.新增…

如何提交网站到谷歌网站收录?

其实就那么几个步骤&#xff0c;要做谷歌那肯定是需要一个谷歌账号的&#xff0c;然后找到Google Search Console这个谷歌的官方平台&#xff0c;这是最权威的可以统计来自谷歌流量的平台了&#xff0c;毕竟是谷歌自家的&#xff0c;肯定也不可能作假&#xff0c;然后就是跟着平…

使用DockerFile 编写 指令来构建镜像

文章目录 前言使用DockerFile 编写 指令来构建镜像1. 构建2. 验证 前言 如果您觉得有用的话&#xff0c;记得给博主点个赞&#xff0c;评论&#xff0c;收藏一键三连啊&#xff0c;写作不易啊^ _ ^。   而且听说点赞的人每天的运气都不会太差&#xff0c;实在白嫖的话&#x…

二进制部署k8s集群 部署高可用master节点

目录 本次部署的环境 一、master02 节点部署 二、负载均衡部署 安装nginx服务 部署keepalive服务 修改node节点上的配置文件 在master节点上创建pod 三、部署 Dashboard 二进制部署k8s集群部署的步骤总结 &#xff08;1&#xff09;k8s的数据存储中中心的搭建 etcd &…

UNION的使用

UNION的使用 给出将多条查询语句组合成单个结果集&#xff0c;两个表对应的列数和数据类型必须相同 UNION操作符&#xff1a; 返回两个查询结果集的并集&#xff0c;并去除重复记录 UNION ALL操作符 返回两个查询的结果集的并集。不去掉两个结果集的重复部分&#xff0c;重…

【Linux】Linux信号产生,接受与处理机制

理解Linux信号产生&#xff0c;接受与处理机制 信号是Linux操作系统中一种用于进程间通信和异步事件处理的机制。在本文中&#xff0c;我们将结合Linux的源码&#xff0c;深入分析信号的产生、发送、接收和处理的底层原理。 文章目录 理解Linux信号产生&#xff0c;接受与处理…

一、QGroundControl地面站使用介绍

文章目录 环境功能介绍飞行视图规划视图飞机设置分析工具程序设置 连接飞机飞机设置分析工具飞行视图规划任务 总结参考 环境 QGroundControl V4.2.0PX4-Autopilot V1.3.0devGazebo 模拟无人机 功能介绍 飞行视图规划视图飞机设置分析工具程序设置 飞行视图 软件打开后为飞…

跟着鲁sir学CV_Opencv(10)卡尔曼滤波

简介 卡尔曼滤波器由鲁道夫卡尔曼&#xff08;Rudolf E. Klmn&#xff09;在1960年提出&#xff0c;广泛应用于导航系统、信号处理、机器人定位、金融等多个领域。 主要分为两阶段&#xff1a;预测与更新 贝叶斯滤波器 贝叶斯框架下&#xff1a;预测&#xff08;先验&#x…

楼道堆积物视觉识别监控系统

楼道堆积物视觉识别监控系统采用了AI神经网络和深度学习算法&#xff0c;楼道堆积物视觉识别监控系统通过摄像头实时监测楼道的情况&#xff0c;通过图像处理、物体识别和目标跟踪算法&#xff0c;系统能够精确地识别楼道通道是否被堆积物阻塞。楼道堆积物视觉识别监控系统检测…

K210的MicroPython扩展例程——自动驾驶例程(视觉循迹)

前言 该例程实现的功能是循迹功能&#xff0c;可为想拿K210做视觉循迹开发作为参考 例程使用前需要搭建好MicroPython的开发环境 K210开发板MicroPython开发环境搭建 一、将K210连接好后打开CanMV IDE&#xff0c;连接成功后&#xff0c;三角形变成绿色 二、然后要把小车驱动…

接口测试怎么测?为什么要做接口测试?

一、前言 接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间及内部各个子系统之间的交互点。测试的重点是检查数据的交换、传递和控制管理过程&#xff0c;以及系统间的逻辑依赖关系等。 简单地说&#xff0c;接口测试就是通过URL向服务器或者…

leetCode-hot100-数组专题之区间问题

数组专题之区间问题 知识点&#xff1a;解决思路&#xff1a;例题56.合并区间57.插入区间253.会议室 Ⅱ485.无重叠区间 数组区间问题是算法中常见的一类问题&#xff0c;它们通常涉及对数组中的区间进行排序、合并、插入或删除操作。无论是合并区间、插入区间还是删除重复空间&…

azure gpt 技术教程教学 | 在Azure OpenAI 上部署GPT-4o

Azure OpenAI GPT-4o是OpenAI推出的最新旗舰级人工智能模型。GPT-4o模型设计为能够实时对音频、视觉和文本进行推理&#xff0c;这是迈向更自然人机交互的重要一步。该模型的一大特点是能够处理多种类型的数据输入和输出&#xff0c;包括文本、音频和图像&#xff0c;实现了跨模…

适用于Windows 电脑的最佳视频恢复软件和方法

毫无疑问&#xff0c;丢失您的基本数据总是有压力的&#xff0c;尤其是当这些是您为捕捉最美好回忆而收集的重要视频文件时。要恢复丢失或损坏的视频文件&#xff0c;您可以借助视频恢复工具。但是&#xff0c;在选择最佳视频恢复工具时&#xff0c;您必须考虑多个扫描选项&…

Windows10安装python3.8.2

1、下载与安装 下载地址&#xff1a;https://www.python.org/downloads/release/python-382/ 滑动到页面底部 下载好的安装包安装到合适的位置&#xff08;默认安装到C盘&#xff09; 安装的时候&#xff0c;见到Add Python 3.8 to PATH 记得勾选. 2、检测安装是否成功 cm…