【Linux】多线程同步与互斥

news2025/1/12 18:45:18

目录

  • 🌈前言
  • 🌸1、Linux线程同步
    • 🍨1.1、同步概念与竞态条件
    • 🍧1.2、条件变量
  • 🌺2、条件变量相关API
    • 🍨2.1、初始化和销毁条件变量
    • 🍧2.2、阻塞等待条件满足
    • 🎃2.3、唤醒阻塞等待的条件变量
    • 🎂2.4、为什么 pthread_cond_wait 需要互斥锁?⭐⭐⭐
  • 🍀3、生产者消费者模型
    • 🍨3.1、概念
    • 🍧3.2、基于BlockingQueue的生产者消费者模型
    • 🎃3.3、阻塞队列的实现

🌈前言

这篇文章给大家带来线程同步与互斥的学习!!!


🌸1、Linux线程同步

🍨1.1、同步概念与竞态条件

首先抛出一个问题:线程互斥,它是对的,但是它合理(任何场景)吗??? 答:不一定合理

举个例子⭐⭐⭐
  • 我们去食堂打饭,食堂打饭的规则是竞争式的抢饭(不用排队)
  • 力气大的人会优先去抢到饭(男生 --优先级高的线程),力气小的就会一直抢不到饭(女生 – 优先级小的线程)
  • 这种规则没有错,确实食堂阿姨一次只能给一个人打饭,但是不合理,会造成弱小的人的饥饿问题(迟迟没有吃到饭)!!!
  • 在多线程竞争锁来看,优先级高的线程会一直优先申请到锁资源,而优先级低的线程会长时间得不到对应的资源,会造成多执行流下的饥饿问题!!!
  • 互斥下的饥饿问题:多线程下的某个执行流,长时间得不到某种资源

同步概念⭐⭐⭐
  • 同步:在保证数据安全的前提下(互斥),让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
  • 竞态条件:因为时序(CPU调度)问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解
  • 同步与互斥互帮互助的,互斥是解决线程安全问题,而同步是解决合理性问题

🍧1.2、条件变量

我们已经直到同步是什么了,那么如何实现同步与互斥呢? 答:条件变量

概念:⭐⭐⭐
  • 当一个线程互斥地访问临界资源时,需要另一个线程对临界资源的状态做改变,就需要条件变量了
  • 例如:一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
  • 当条件(由程序员设置)满足时,设置的条件变量就会阻塞等待执行该函数的线程,并且释放锁,随后下一个线程就会申请到锁继续判断

下面代码看看就好,后面会讲

pthread_mutex_lock()
if (YES/NO)
{
	pthread_cond_wait()
}
// ....做其他事情
pthread_cond_signal() // 或者唤醒其他线程, 也可以在主线程判断唤醒
pthread_mutex_unlock(); // 解锁

🌺2、条件变量相关API

🍨2.1、初始化和销毁条件变量

初始化条件变量有二种方法

第一种方法:静态分配

#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
代码解析
  • pthread_cond_t是条件变量,它是一个联合体,里面有一个结构体描述条件变量的属性
  • PTHREAD_COND_INITIALIZER:它是一个,用于初始化条件变量
  • 注意:静态分配不用释放条件变量

第二个方法:动态分配

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, 
							const pthread_condattr_t *restrictattr);
函数解析
  • cond:要初始化的条件变量(pthread_mutex_t变量的地址
  • restrictattr:设置条件变量的属性,一般为NULL/nullptr
  • 返回值:初始化成功返回0,失败返回一个错误码errno
  • 注意:动态分配需要释放条件变量

销毁条件变量:

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond)
函数解析
  • cond:要销毁的条件变量(pthread_cond_t变量的地址
  • 返回值:初始化成功返回0,失败返回一个错误码errno
销毁条件变量需要注意
  • 使用 PTHREAD_ COND_ INITIALIZER 初始化的条件变量不需要销毁
  • 使用 pthread_cond_init初始化的条件变量,需要进行销毁

🍧2.2、阻塞等待条件满足

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, 
						 pthread_mutex_t *restrict mutex);
函数解析
  • cond:阻塞要在这个条件变量上等待的线程(pthread_cond_t变量的地址
  • mutex:互斥锁,同步需要与互斥锁绑定使用,因为阻塞等待时,会释放该线程的锁,后面被唤醒时,会重新获取锁,后面代码感受
  • 返回值:成功完成后,返回零值;否则,返回错误编号(errno)以指示错误

🎃2.3、唤醒阻塞等待的条件变量

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
函数解析
  • cond:唤醒在条件变量上等待的线程(pthread_cond_t变量的地址
  • pthread_cond_broadcast:唤醒被阻塞等待的全部线程
  • pthread_cond_signal:唤醒被阻塞等待的一个线程,按顺序唤醒
  • 返回值:如果成功,pthread_cond_broadcast()和pthread-cond_signal()函数返回零;否则应返回一个错误编号(errno)以指示错误

使用同步与互斥实现多线程间轮询运行,在主线程唤醒被条件变量阻塞的线程

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using namespace std;

// 定义条件变量
pthread_cond_t pc;
// pthread_cond_t pc = PTHREAD_COND_INITIALIZER;   // 静态初始化
// 定义互斥锁
pthread_mutex_t pm;
// pthread_mutex_t pm = PTHREAD_MUTEX_INITIALIZER; // 静态初始化

// 定义全局退出变量 -- volatile保持内存可见性
volatile bool quit = false;

// 定义一个方法集合
typedef void (*Func)();
vector<Func> handler;

void Print()
{
    cout << "hello Print" << endl;
}

// 线程执行函数
void *CallThread(void *args)
{
    // 线程分离,线程退出后自动释放资源,主线程不用等待
    pthread_detach(pthread_self());
    string name = static_cast<const char *>(args);
    while (!quit)
    {
        // 执行这个函数,说明特定的条件变量没有就绪,执行该函数的线程会被阻塞挂起等待
        pthread_cond_wait(&pc, &pm);
        cout << name << ": " << pthread_self() << ", run..." << endl;
        // 执行方法集合的全部方法
        for (auto &f : handler)
        {
            f();
        }
    }
    return nullptr;     
}

int main()
{
    // 加载函数方法 -- 第二个是lambda表达式(底层是仿函数operator())
    handler.push_back(Print);
    handler.push_back([]()
                      { cout << "hello lambda" << endl; }
                      );
    // 初始化条件变量和互斥锁
    pthread_mutex_init(&pm, nullptr);
    pthread_cond_init(&pc, nullptr);
    // 线程创建
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, CallThread, (void *)"Thread1");
    pthread_create(&t2, nullptr, CallThread, (void *)"Thread2");
    pthread_create(&t3, nullptr, CallThread, (void *)"Thread3");

    while (true)
    {
        char ch = '\0';
        cout << "请输入一个字符y/n: ";
        cin >> ch;
        if (ch == 'y')
        {
            //  该函数可以让条件变量就绪,唤醒单个被阻塞挂起线程
            pthread_cond_signal(&pc);
            sleep(1);
        }
        else
        {
            quit = true;
            // 唤醒所有被阻塞挂的线程
            pthread_cond_broadcast(&pc);
            break;
        }
    }
    // 释放条件变量和互斥锁
    pthread_cond_destroy(&pc);
    pthread_mutex_destroy(&pm);
    return 0;
}

从运行结果图可以看出线程是按顺序轮询执行的…

在这里插入图片描述

为什么输入n后,唤醒全部线程后,最后不是打印三次方法集合的信息呢?

  • 因为全部线程被唤醒后,又会重新去竞争锁(条件变量需要重新申请锁),只有一个线程可以竞争成功,没有竞争成功的线,会重新判断!quit,!quit为false,退出执行函数

  • 这个线程执行完方法集合发现!quit为false不能进入循环,就退出了,届时,全部线程就已经退出了,只有一个线程执行了方法集合!!!





🎂2.4、为什么 pthread_cond_wait 需要互斥锁?⭐⭐⭐

条件与条件变量
  • 条件:对应的共享资源的状态,(比如抢票,票数小于0,就不能抢了),通过判断的方式,来判断对应的资源是否符合要求
  • 条件变量:在条件满足或不满足的前提下,进行wait(等待) 或 signal(唤醒)的一种方式

结论
  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足
  • 所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知(唤醒)等待在条件变量上的线程
  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据

在这里插入图片描述

按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码

// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) 
{
	pthread_mutex_unlock(&mutex);
	// 解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过 -- 发生线程切换
	pthread_cond_wait(&cond);
	pthread_mutex_lock(&mutex);
} 
pthread_mutex_unlock(&mutex);
结论
  • 由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是一个原子操作
  • int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样 – 原子操作

条件变量使用规范

  • 等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
  • 给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

🍀3、生产者消费者模型

🍨3.1、概念

概念

  • 生产者消费者模式就是通过一个容器(链表或队列)来解决生产者和消费者的强耦合问题

  • 生产者和消费者彼此之间不直接通讯,而是通过阻塞队列来进行通讯

  • 所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取

  • 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的

在这里插入图片描述


如果还是不理解的话,请看下面的例子

-

消费者线程有多个,消费者之间是什么关系呢?

  • 它们之间是竞争的关系(商品少的时候,就要竞争) – 互斥

供应商线程有多个,供应商之间是什么关系呢?

  • 它们之间是竞争的关系(供应商要竞争超市的架子,摆上自己的商品) – 互斥

消费者和供应商之间又是什么关系呢?

  • 它们之间是同步与互斥的关系,因为消费者去购买商品时,可能出现缺货的情况,这时候就要通知供应商供给商品了,反之,生产者供给商品后,也要通知消费者来买…

总结:⭐⭐⭐⭐⭐

  • 321原则:3种关系(消费者与消费者之间的关系,生产者和生产者之间的关系,消费者和生产者之间的关系),2种角色(消费者和生产者),一个交易场所(超市)

生产者消费者模型优点:解耦、支持并发、支持忙闲不均


🍧3.2、基于BlockingQueue的生产者消费者模型

BlockingQueue:

  • 在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构

  • 其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素(这里要设置条件判空,设置条件变量)

  • 当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

在这里插入图片描述


🎃3.3、阻塞队列的实现

实现方式:

  • 主要问题:消费者和生产者的三种关系要控制好

  • 唤醒各自线程的时机,不要乱加锁,可能造成死锁或一直等待的问题

blockqueue.hpp

#include <iostream>
#include <queue>
#include <unistd.h>
#include <pthread.h>
using namespace std;

const int DfCap = 5; // 默认容量大小

namespace Mybq
{
    template <typename T>
    class BlockQueue
    {
    public:
        BlockQueue(uint32_t cap = DfCap)
            : _cap(cap), _bq()
        {
            pthread_mutex_init(&mutex, nullptr);
            pthread_cond_init(&conCond, nullptr);
            pthread_cond_init(&proCond, nullptr);
        }

        // 生产者生产
        void push(const T &val)
        {
            // 加锁 -- 保护临界资源(队列)
            lockQueue();
            // 循环判断阻塞队列是否为满 -- 防止伪唤醒 -> proPendwait调用失败、多线程竞争锁或系统原因等等...
            while (isFull())
            {
                // 等待的时候,自动释放mutex锁
                proPendwait(); // 阻塞等待,等待被唤醒
                // 等待完后,是在临界区醒的,重新申请mutex锁
            }
            // 加载资源 -> 解锁
            _bq.push(val);
            unlockQueue();
            // 生产者唤醒消费者,因为生产者已经把资源放到队列里面了(条件变量就绪)
            weakupJCon();
        }
        
        // 消费者消费
        T pop()
        {
            lockQueue();
            // 防止伪唤醒
            while (isEmpty())
            {
                // 阻塞等待,等待被唤醒
                conPendWait();
            }
            // 删除资源->解锁
            T val = _bq.front();
            _bq.pop();
            unlockQueue();
            // 消费者唤醒生产者,因为队列的资源没有了(条件变量就绪)
            weakupPro();
            return val;
        }

        ~BlockQueue()
        {
            pthread_mutex_destroy(&mutex);
            pthread_cond_destroy(&conCond);
            pthread_cond_destroy(&proCond);
        }

    private:
        // 判断阻塞队列是否为空,是否为满
        bool isFull()
        {
            return _bq.size() == _cap;
        }

        bool isEmpty()
        {
            return _bq.empty();
        }
        //-------------------------------------------

        // 互斥锁加锁解锁
        void lockQueue()
        {
            pthread_mutex_lock(&mutex);
        }

        void unlockQueue()
        {
            pthread_mutex_unlock(&mutex);
        }

        //--------------------------------------------

        // 在条件变量下阻塞等待,等待被唤醒
        void proPendwait()
        {
            // 1. 条件变量在阻塞等待线程的时候,会自动释放mutex互斥锁!!!
            // 释放锁:因为阻塞等待的线程占用着锁,其他线程不能申请 -- 没有锁了意味着不能访问临界资源
            pthread_cond_wait(&proCond, &mutex);
            // 2. 当阻塞结束(唤醒),返回时,pthread_cond_wait,会自动帮你重新获取mutex锁,最后才返回
        }

        void conPendWait()
        {
            pthread_cond_wait(&conCond, &mutex);
        }

        //--------------------------------------------

        // 条件变量就绪,被唤醒阻塞等待的程序
        void weakupPro()
        {
            // 消费者唤醒生产者,因为队列的资源没有了(消费者调用)
            pthread_cond_signal(&proCond);
        }

        void weakupJCon()
        {
            // 生产者唤醒消费者,因为生产者已经把资源放到队列里面了(生产者调用)
            pthread_cond_signal(&conCond);
        }

    private:
        uint32_t _cap; // 阻塞队列容量大小
        queue<T> _bq;
        pthread_mutex_t mutex;  // 阻塞队列互斥锁
        pthread_cond_t conCond; // 消费者条件变量
        pthread_cond_t proCond; // 生产者条件变量
    };
}

test.cpp

#include "blockqueue.hpp"
#include <ctime>
#include <cstdlib>

// 生产者线程执行函数
void *CallProducer(void *args)
{
    Mybq::BlockQueue<int>* bqp = static_cast< Mybq::BlockQueue<int>*>(args);
    while (true)
    {
    // 生产数据 -- [0, 100]
    int data = rand() % 100 + 1;
    bqp->push(data);
    cout << "Producer data sucess, data: " << data << endl;
    sleep(2);
    }
    return nullptr;
}

// 消费者线程执行函数
void *Callconsumer(void *args)
{
    Mybq::BlockQueue<int>* bqp = static_cast< Mybq::BlockQueue<int>*>(args);
    while (true)
    {
    // 消费数据
    int data = bqp->pop();
    cout << "Consumer data sucess, data: " << data << endl;
    }
    return nullptr;
}

int main()
{
    srand((unsigned int)time(nullptr)); // 随机数种子
    Mybq::BlockQueue<int> bq;
    // 多线程测试
    pthread_t producer; // 生产者线程
    pthread_t consumer; // 消费者线程
                        // &bq将阻塞队列的地址传给线程执行函数的参数
    pthread_create(&producer, nullptr, CallProducer, &bq);
    pthread_create(&consumer, nullptr, Callconsumer, &bq);
    // 等待线程
    pthread_join(producer, nullptr);
    pthread_join(consumer, nullptr);
    return 0;
}

在这里插入图片描述

总结

  • 生产者消费者模型的并发优点其实是体现在生产线程制作数据和消费线程消费数据的时候,因为它们没有被互斥,会并发的去执行,提高效率

在这里插入图片描述

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

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

相关文章

python数据可视化开发:Matplotlib库参数配置基础知识

文章目录前言01.工具栏组件02.数据03.设置字体字典&#xff08;1&#xff09;全局字体样式&#xff08;2&#xff09;常用中文字体对应名称&#xff08;3&#xff09;查询当前系统所有字体04.图像配置实例05.图表标题06.文本组件07.坐标轴标签组件08.网格组件09.绘制折线10.图例…

传染疾病模型

1 分支过程 1.1 工作原理 第一波疫情 假设一个人携带一种新的病毒&#xff0c;以独立的概率p将疾病传染给遇到的每一个人假设这个人在感染期遇到了k个人 ——>这k个人是该疾病传染的第一波基于疾病是随机传染的&#xff0c;所以第一波中有些人会感染疾病&#xff0c;有些人…

一篇基于深度学习的命名实体识别技术的研究报告

一篇基于深度学习的命名实体识别技术的研究报告 本篇文章主要是自己刚接触NER领域时&#xff0c;研读这篇《 A Survey on Deep Learning for Named Entity Recognition 》NER综述论文时翻译的中文版&#xff0c;这篇综述时间是2020年&#xff0c;可能近两年的部分成果暂未包含…

Python数据可视化(一)图表组成元素

1.1绘制 matplotlib 图表组成元素的主要函数matplotlib 是如何组织内容的&#xff1f;在一个图形输出窗口中&#xff0c;底层是一个 Figure实例&#xff0c;我们通常称之为画布&#xff0c;包含一些可见和不可见的元素。在画布上&#xff0c;自然是图形&#xff0c;这些图形就是…

Java---微服务---RabbitMQ部署

RabbitMQ部署1.单机部署1.1.下载镜像1.2.安装MQ1.3访问管理端2.集群部署2.1.集群分类2.2.设置网络1.单机部署 我们在Centos7虚拟机中使用Docker来安装&#xff0c;如未安装dockr&#xff0c;请参考《Centos7安装Docker》 1.1.下载镜像 方式一&#xff1a;在线拉取 docker …

剑指Offer 第3天、第4天

剑指 Offer 05. 替换空格 class Solution { public:string replaceSpace(string s) {string res;for(auto e : s){if(e ){res%;res2;res0;}elserese;}return res;} }; 剑指 Offer 58 - II. 左旋转字符串 class Solution { public:string reverseLeftWords(string s, int n) {…

java spring IOC xml方式注入对象类型的list集合数据

我们新创建一个java项目 然后引入spring的基本依赖 在src下创建一个collectiontype 包 在 collectiontype 包下创建一个 Course类 参考代码如下 package collectiontype;//课程类 public class Course {//课程名称private String cname;public void setCname(String cname) …

Java---微服务---RabbitMQ入门与应用

RabbitMQ入门与应用1.初识MQ1.1.同步和异步通讯1.1.1.同步通讯1.1.2.异步通讯1.2.技术对比&#xff1a;2.快速入门2.1.安装RabbitMQ2.2.RabbitMQ消息模型2.3.导入Demo工程2.4.入门案例2.4.1.publisher实现2.4.2.consumer实现2.5.总结3.SpringAMQP3.1.Basic Queue 简单队列模型3…

浅析一条SQL在mysql中是如何执行的

一. Mysql内部组件结构 MySql大体分为server层和存储引擎层&#xff0c; server层 主要包括连接器、查询缓存、分析器、优化器、执行器等&#xff0c;涵盖 MySQL 的大多数核心服务功能&#xff0c;以及所有的内置函数&#xff08;如日期、时间、数学和加密函数等&#xff09;…

spring boot整合redis中间件与热部署实现

热部署 每次写完程序后都需要重启服务器&#xff0c;需要大量的时间&#xff0c;spring boot提供了一款工具devtools帮助实现热部署。 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId>…

1. python包管理pip工具

1. 何为pip&#xff1f; pip 是 python包管理工具&#xff0c;该工具提供了对 python包的查找、下载、安装、卸载的功能。 目前最新的 python版本已经预装了 pip。注意&#xff1a;python 2.7.9 或 python 3.4 以上版本都自带 pip 工具。之前在基础篇中我们已经安装了python3…

Java 23种设计模式(6.结构型模式-适配器模式)

结构型模式-适配器模式 代码分析 类图 代码 public class Target {//就是客户期待的接口&#xff0c;目标可以是具体&#xff0c;抽象的类&#xff0c;也可以是接口public String Request(){String msg "normal request";return msg;} }public class Adaptee {pub…

零基础学JavaWeb开发(二十五)之 vue快速入门

一、什么是VUE Vue 是一套用于构建用户界面的渐进式JavaScript框架&#xff0c;简化dom操作。 基于MVVM 是Model-View-ViewModel 的缩写&#xff0c;它是一种基于前端开发的架构模式&#xff0c;其核心是提供对View 和 ViewModel 的双向数据绑定&#xff0c;这使得ViewModel …

【头歌】循环单链表的基本操作

循环单链表的基本操作第1关&#xff1a;循环单链表的插入操作任务描述本关任务&#xff1a;编写循环单链表的插入操作函数。相关知识对于单链表&#xff0c;每个结点只存储了其后继结点的地址。尾结点之后不再有任何结点&#xff0c;那么它的next域设置有两种方式&#xff1a;将…

Python爬虫网页解析神器Xpath详细讲解

1、XPath介绍 XPath 是一门在 XML 文档中查找信息的语言。最初是用来搜寻 XML 文档的&#xff0c;但同样适用于 HTML 文档的搜索。 2、安装lxml lxml是Python的第三方解析库&#xff0c;支持HTML和XML解析&#xff0c;而且效率极高&#xff0c;弥补了Python自带的xml标准库在…

Mybatis-Plus id生成策略控制

目录 id生成策略控制 不同的表应用不同的id生成策略 名称 TableId AUTO策略 除了AUTO这个策略以外&#xff0c;还有如下几种生成策略: 分布式ID是什么? INPUT策略 ASSIGN_ID策略 ASSIGN_UUID策略 雪花算法 ID生成策略对比 id生成策略控制 不同的表应用不同的id生成…

计算机组成原理 | 第六章:计算机的运算方法 | 进制转换 | 定点运算 | 浮点数运算

文章目录&#x1f4da;进位计数制&#x1f407;任意进制转十进制&#x1f407;十进制整数转换为n进制整数&#x1f407;十进制小数转换为n进制小数&#x1f407;二/八/十六进制的互换&#x1f4da;带符号的二进制数表示⭐️&#x1f407;原码表示法&#x1f407;补码表示法&…

“买卖股票的最佳时机” 系列——我来教你稳赚不亏~

目录 前言 一、买卖股票的最佳时机 ——>指定次数交易&#xff08;1次&#xff09; 1.1、dp定义 1.2、递推公式 1.3、遍历顺序 1.4、初始化 1.5、解题代码 二、买卖股票的最佳时机II ——>交易到结束 2.1、分析 2.2、解题代码 三、买股票的最佳时机III ——>…

【keepass】密码管理软件keepass的安全风险分析,如何在使用keepass的过程中避免泄露数据库信息和密码?

一、安全风险分析 1.1 不正规的来源 如果你使用非官方渠道获得keepass软件或某些插件&#xff0c;那么你的密码管理从一开始就没有安全性可言。因为这玩意是开源的啊&#xff0c;如果对方“很懂”&#xff0c;只要往里面植入一些代码&#xff0c;让你的数据库文件和密钥在后台…

react 项目 中 使用 Dllplugin 打包优化

webpack在build包的时候&#xff0c;有时候会遇到打包时间很长的问题&#xff0c;这里提供了一个解决方案&#xff0c;让打包如丝般顺滑~ 在用 Webpack 打包的时候&#xff0c;对于一些不经常更新的第三方库&#xff0c;比如 react&#xff0c;lodash&#xff0c;vue 我们希望…