【Linux】同步与互斥

news2025/1/14 18:26:30

目录

  • 🌈前言
  • 🌸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、条件变量

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

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

🌺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/178752.html

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

相关文章

2023 年第一弹, Flutter 3.7 发布啦,快来看看有什么新特性

核心内容原文链接&#xff1a; https://medium.com/flutter/whats-new-in-flutter-3-7-38cbea71133c 2023 年新春之际&#xff0c; Flutter 喜提了 3.7 的大版本更新&#xff0c;在 Flutter 3.7 中主要有改进框架的性能&#xff0c;增加一些很棒的新功能&#xff0c;例如&#…

初识网络爬虫

爬虫简介 网络爬虫又称网络蜘蛛、网络机器人&#xff0c;它是一种按照一定的规则自动浏览、检索网页信息的程序或者脚本。网络爬虫能够自动请求网页&#xff0c;并将所需要的数据抓取下来。通过对抓取的数据进行处理&#xff0c;从而提取出有价值的信息。 爬虫简单来说就是是通…

Mongodb基础操作

打开Mongodb服务&#xff0c;打开Robo 3T&#xff0c;链接服务并创建数据库&#xff1a; 创建表&#xff08;集合&#xff09;&#xff1a; 双击打开一个界面&#xff1a; 添加数据 查询book表&#xff1a; 添加属性名&#xff08;新数据&#xff09;&#xff1a; 查询&#xf…

推荐算法入门:序列召回(二)

召回&#xff1a;输入一个用户的&#xff08;点击&#xff09;序列&#xff0c;通过某种方法&#xff08;序列建模的方法&#xff09;&#xff0c;把用户输入的序列变为向量&#xff0c;用用户向量&#xff0c;在所有的item的向量进行快速检索&#xff0c;依次达到序列召回的效…

Linux使用YUM源安装Docker

安装环境查看Linux版本&#xff0c;如图&#xff1a;下载docker yum源登录阿里云开源镜像站&#xff0c;地址如下&#xff1a;阿里云开源镜像站搜索docker&#xff0c;如图&#xff1a;打开docker-ce&#xff0c;如图&#xff1a;复制docker-ce源地址&#xff0c;如下&#xff…

高并发环境如何有效缓解带宽压力

网络带宽是指在单位时间&#xff08;一般指的是1秒钟&#xff09;内能传输的数据量。网络和高速公路类似&#xff0c;带宽越大&#xff0c;就类似高速公路的车道越多&#xff0c;其通行能力越强。   在持续的多用户、高并发的情况下&#xff0c;缓解带宽压力可以避免客户端卡…

IO和NIO

什么是I/O模型: 通常情况下I/O操作是比较耗时的&#xff0c;所以为了高效的使用硬件&#xff0c;应用程序可以专门设置一个线程进行I/O操作&#xff0c;而另外一个线程则利用CPU的空闲去做其他计算&#xff0c;这种为提高应用执行效率而采用的I/O操作方法称为I/O模型&#xff…

【笔记】ASP.NET Core技术内幕与项目实现:基于DDD与前后端分离

最近在写论文&#xff0c;想使用ASP.NET Core Web API技术&#xff0c;但对它还不是很熟&#xff0c;鉴权组件也没用过&#xff0c;于是在网上查找资料&#xff0c;发现了杨中科老师写的这本书&#xff08;微信读书上可以免费看&#xff09;&#xff0c;说起来我最初自学C#时看…

C++:类中const修饰的成员函数

目录 一.const修饰类的成员函数 1.问题引出&#xff1a; 代码段&#xff1a; 2.问题分析 3.const修饰类的成员函数 二. 类的两个默认的&运算符重载 三. 日期类小练习 一.const修饰类的成员函数 1.问题引出&#xff1a; 给出一段简单的代码 代码段&#xff1a; #in…

springcloud3 Sentinel的搭建以及作用

一 sentinel的概念 1.1 sentinel Sentinel是分布式系统流量控制的哨兵&#xff0c;阿里开源的一套服务容错的综合性解决方案。 主要用来处理&#xff1a; 服务降级 服务熔断 超时处理 流量控制 sentinel 的使用可以分为两个部分: 核心库&#xff08;Java 客户端&#…

【软件工程】用例图、状态图与活动图

题目要求&#xff1a; 一、投诉人对广州市燃气行业相关单位的经营和服务不满意或存在意见时&#xff0c;对燃气处或市政园林局服务监督处进行投诉。 二、燃气处投诉专管员受理直接来自投诉人或由服务监督处转来的相关投诉。 三、燃气处投诉专管员落实相关单位&#xff08;或…

DlhSoft Gantt Chart Light Library 4.3.47 Crack

DlhSoft Gantt Chart Light Library 4.3.47 改进了 Microsoft Project XML 文件的加载和图像的导出。 2023 年 1 月 24 日 - 10:09新版本 特征 改进了 Microsoft Project XML 文件的加载和从“ScheduleChartDataGrid”导出图像。 添加了新的“TotalResourceEffort”和“TotalRe…

USART 数据流控制

USART 数据流控制 也就是 USART_HardwareFlowControl 一、流控制的作用 这里讲到的 “流”&#xff0c;指的是数据流&#xff1b;在数据通信中&#xff0c;流控制是管理两个节点之间数据传输速率的过程&#xff0c;以防止出现接收端的数据缓冲区已满&#xff0c;而发送端依然继…

3.5动态规划--凸多边形的最优三角剖分

写在前面 尽管这是一个几何问题&#xff0c;但本质上与3.1-矩阵连乘极为相似 定义dp数组的含义&#xff1a;t[i][j]表述以点Vi-1&#xff0c;Vi&#xff0c;...&#xff0c;Vj为顶点的最优三角形剖分的最优权函数值 我们要计算的最优值在 t[1][n] 递归结构&#xff1a;凸多…

通过Moonbeam的Connected Contracts互连合约从Axelar转移Token至Centrifuge

将Moonbeam预编译智能合约功能与波卡指定技术交互&#xff0c;再结合Axelar通用消息传递&#xff08;GMP&#xff09;&#xff0c;能够实现其他链无法完成的独特交互。阅读本文了解Connected Contracts互连合约如何通过只与单条链交互连接Axelar的EVM链发送Token至Centrifuge等…

四、新图片、新视频预测(Datawhale组队学习)

文章目录配置环境预测新图像载入图像并进行预处理导入训练好的模型前向预测将分类结果写入原图中预测新视频导入训练好的模型视频预测单帧图像分类预测可视化方案一&#xff1a;原始图像预测结果文字可视化方案二&#xff1a;原始图像预测结果文字各类别置信度柱状图预测摄像头…

Mybatis 基本使用案例

1、基本的CRUD 1.1、新增 <!--int insertUser();--> <insert id"insertUser"> insert into t_user values(null,admin,123456,23,男) </insert> 1.2、删除 <!--int deleteUser();--> <delete id"deleteUser"> delete fro…

【docker概念和实践 4】容器命令和案例(2)

一、说明 docker的四个要素是&#xff1a;本地的Docker-engine、网上&#xff08;本地&#xff09;的仓库、镜像images、容器&#xff1b;初学者必须了解这是个概念的关系。但是&#xff0c;真正重要的概念是容器&#xff0c;因为&#xff0c;只有掌握了容器&#xff0c;才能具…

3.2主存储器的基本组成

文章目录一、引子二、半导体元件1.基本半导体元件&#xff08;1&#xff09;MOS管&#xff08;2&#xff09;电容2.读写二进制数&#xff08;1&#xff09;读出二进制①二进制1②二进制0&#xff08;2&#xff09;写入二进制3.存储体三、存储芯片的基本原理1.译码器2.控制电路3…

由Bitlocker问题引发的思考

由Bitlocker问题引发的思考一、什么是Bitlocker问题二、如何解决Bitlocker问题三、萌生的思考一、什么是Bitlocker问题 Bitlocker概述 BitLocker 驱动器加密是一项数据保护功能&#xff0c;它与操作系统集成&#xff0c;用于解决来自丢失、被盗或销毁不当的计算机的数据被盗或…