Linux : 多线程互斥

目录

一  前言

二 线程互斥

  三  Mutex互斥量

1. 定义一个锁(造锁) 

2. 初始化锁

3. 上锁

4. 解锁

5. 摧毁锁

四 锁的使用

五 锁的宏初始化 

六 锁的原理

1.如何看待锁?

2. 如何理解加锁和解锁的本质 

七 c++封装互斥锁

八 可重入与线程安全 

1. 可重入与线程安全联系

2. 可重入与线程安全区别

九 死锁

1.死锁产生的必要条件

2.死锁的避免方法


一  前言

我们在上一章节Linux: 线程控制-CSDN博客学习了什么是多线程,以及多线程的控制和其优点,多线程可以提高程序的并发性和运行效率。但是多线程控制也有一定缺点,例如有些多线程的程序运行结果是有一些问题的,如出现了输出混乱、访问共享资源混乱等特点。所以我们下面提出的这个概念是关于保护共享资源这方面的——线程互斥。


二 线程互斥

在正式认识线程互斥之前,我们先来介绍几个概念:

  • 临界资源:多线程执行流共享的资源(且这个资源是被保护的)就叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么不做,要么做完。(原子性针对的是操作)。

接下来我们用一个测试来学习多线程访问共享资源可能带来的问题,以及如何解决。

🚀:系统调用接口大多都是用c接口,我们通过c/c++混编的方式对线程的创建以及等待进行封装

//makefile/
mythread:mythread.cppg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f mythread
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include <cstdio>
#include <cassert>///Thread.hpp/头文件///
class Thread; //对类进行声明//上下文
class Context
{
public:Thread* this_;void* args_;
public:Context():this_(nullptr),args_(nullptr){}~Context(){}
};
//对一个线程进行封装
class Thread
{
public:typedef std::function<void*(void*)> func_t;const int num =1024;
public://1.构造函数,完成对线程的创建Thread(func_t func, void* args, int number):func_(func),args_(args)//c++自带类型func_,所以可以用拷贝构造func_(func){char buffer[num];//字符数组,char* buffersnprintf(buffer, sizeof(buffer), "thread->%d", number);name_=buffer;Context* ctx=new Context();ctx->this_=this;ctx->args_=args_;int n=pthread_create(&tid_,nullptr,start_routine,ctx);assert(n==0);(void)n;}static void* start_routine(void* args){//静态方法不能调用成员变量//return func_(args);Context* ctx=static_cast<Context*>(args);void* ret=ctx->this_->run(ctx->args_);delete ctx;return ret;}//线程等待void join(){int n=pthread_join(tid_,nullptr);assert(n==0);(void)n;}void* run(void* args){return func_(args);}private:std::string name_;pthread_t tid_;func_t func_;//实现方法void* args_;
};
#include <iostream>
#include <unistd.h>
#include <memory>
#include "Thread.hpp"
//测试用例//
int tickets=10000;
void* getTickets(void* args)
{std::string username=static_cast<const char*>(args);while(true){ if(tickets >0){//usleep(1244);std::cout<< username <<"我正在抢票"<<tickets--<< std::endl;usleep(1244);}else{break;}}return nullptr;
}
int main()
{std::unique_ptr<Thread> thread1(new Thread(getTickets,(void*)"user1",1));std::unique_ptr<Thread> thread2(new Thread(getTickets,(void*)"user2",2));std::unique_ptr<Thread> thread3(new Thread(getTickets,(void*)"user3",3));std::unique_ptr<Thread> thread4(new Thread(getTickets,(void*)"user4",4));thread1->join();thread2->join();thread3->join();thread4->join();
}

测试结果

 🚌:接下来我们对代码进行一定改动,再看一下运行结果

 while(true){ if(tickets >0){usleep(1244);//现在我们在进行tickets--操作之前让线程进行休眠//再来看看运行结果会和之前的一样吗?std::cout<< username <<"我正在抢票"<<tickets--<< std::endl;//usleep(1244);//之前的代码是在这里进行了休眠,}else{break;}}

从结果来看,我们放票了10000张照片,而竟然抢到了-2张票,明显不合理。接下来我们回答一下为什么会出现这种现象? 

 代码中凡是关于算数计算的问题,实际上都是交给CPU进行执行的,这里面包括了加减乘除、逻辑运算、逻辑判断,最终都由CPU来解决的。对变量tickets进行--,看起来只有一条语句,但是汇编至少是三条语句,即cpu 会对 tickets-- 的操作会分成三步来执行 

  1. 从内存读取数据到cpu寄存器中
  2. 在寄存器中让cpu进行对应的逻辑判断和运算
  3. 将新的结果写到内存中变量的位置

接下来我们用下面图进行说明,为了方便,假设我们只有两个线程。

 

上面就是该程序出错的原因,其主要原因是在判断 tickets>0 由于会调用其他线程,从而使得错误发生在 tickets-- 操作上,对票的数量修改产生混乱。 

 🚴造成这种结果的原因是什么呢?

  1. 我们对共享资源的访问和修改都不是原子的(即没有做完),这两个操作都会存在中间态,即CPU在计算的过程中需要读取、计算、返回等多个操作,一旦CPU执行某个线程处在某个中间状态的时候暂停了,其他线程可能会“趁虚而入”。
  2. 存在多个线程同时访问共享资源的情况。

  三  Mutex互斥量

 了解了程序出现问题的原因,下来我们就讨论如何解决它:我们先从如何防止多个线程同时访问共享资源开始

       代码必须要有互斥行为:当一个线程访问并执行共享资源的代码时,其他线程不能进入

要想使线程具有互斥行为,我们要引出一个关键工具——,通过给执行共享资源区上一把锁,从而阻止其他线程进入,这种锁被称为互斥锁,给予代码互斥的效果。

 锁的接口及其使用

pthread 库为我们提供了 “定义一个锁”、“初始化一个锁   “上锁”、“解锁”、“销毁一个锁” 的接口:

1. 定义一个锁(造锁) 

pthread_mutex_t  是一个类型,可以来定义一个互斥锁。就像定义一个变量一样使用它定义互斥锁的时候,锁名可以随便设置。互斥锁的类型 pthread_mutex_t 是一个联合体。 

2. 初始化锁

pthread_mutex_init( ) 是pthread库提供的一个初始化锁的一个接口,第一个参数传入的就是需要初始化的锁的地址。 第二个参数需要传入锁初始化的属性,在接下来的使用中暂时不考虑,使用默认属性即传入nullptr 。成功返回0,否则返回错误码。 

3. 上锁

pthread_mutex_lock() ,阻塞式上锁,即 线程执行此接口,指定的锁已经被锁上了,那么线程就进入阻塞状态,直到解锁之后,此线程再上锁。当上锁成功,则返回0,否则返回一个错误码。

4. 解锁

pthread_mutex_unlock() ,作用是解锁接口,一般用于出了执行共享资源区的时候。当解锁成功,返回0,否则返回一个错误码。

5. 摧毁锁

pthread_mutex_destroy 是用来摧毁定义的锁,参数需要传入的是需要摧毁的锁的指针。成功则返回0,否则返回错误码。


四 锁的使用

#include <iostream>
#include <unistd.h>
#include <memory>
#include <vector>
#include "Thread.hpp"class ThreadData
{
public:ThreadData(const std::string & threadname,pthread_mutex_t* mutex_p):threadname_(threadname), mutex_p_(mutex_p){}~ThreadData(){}
public:std::string threadname_;pthread_mutex_t* mutex_p_;//锁的指针
};
int tickets=10000;
void* getTickets(void* args)
{//std::string username=static_cast<const char*>(args);//加锁和解锁是多个线程串行执行的,程序变慢了。ThreadData* td=static_cast<ThreadData*>(args);while(true){ //加锁pthread_mutex_lock(td->mutex_p_);if(tickets >0){usleep(1244);std::cout<< td->threadname_ <<"我正在抢票"<<tickets<< std::endl;tickets--;   pthread_mutex_unlock(td->mutex_p_);//解锁// usleep(1244);}else{  pthread_mutex_unlock(td->mutex_p_);//解锁break;}}return nullptr;
}int main()
{#define NUM 4pthread_mutex_t lock;//定义一个锁pthread_mutex_init(&lock,nullptr);//初始化一个锁//接下来我们如何把这个锁以及一些参数传递给线程呢?我们创建一个类ThreadDatastd::vector<pthread_t> tids(NUM);for(int i=0;i<NUM;i++){char buffer[64];snprintf(buffer,sizeof buffer,"thread %d",i+1);ThreadData* td=new ThreadData(buffer,&lock);//用的同一把锁pthread_create(&tids[i],nullptr,getTickets,td);}for( const auto &tid:tids){pthread_join(tid,nullptr);}pthread_mutex_destroy(&lock);//销毁锁return 0;}

测试结果 

可以看到同过加锁的操作对共享资源的代码进行加锁保护之后,程序已经能正常的进行抢票了。但是我们又发现一个问题,那就是都是线程4进行抢票,这是为什么呢? 

🚩:锁只规定互斥访问,没有规定谁优先执行 ,接下里我们通过修改一下代码,来进行测试。
 通过usleep(1000),线程在访问加锁的资源之后,进行休眠即阻塞状态,这个时候cpu会对其他线程进行随机调度,从而实现了多个进程对保护的共享资源进行抢票的过程 。

while(true){ //加锁pthread_mutex_lock(td->mutex_p_);if(tickets >0){usleep(1244);std::cout<< td->threadname_ <<"我正在抢票"<<tickets<< std::endl;tickets--;   pthread_mutex_unlock(td->mutex_p_);//解锁// usleep(1244);}else{  pthread_mutex_unlock(td->mutex_p_);//解锁break;}//抢完票之后,我们设置一个任务。例如形成订单usleep(1000);//形成一个订单给用户//通过usleep(1000),线程在访问加锁的资源之后,进行休眠即阻塞状态,这个时候cpu会对其他线程进行随机调度//从而实现了多个进程对保护的共享资源进行抢票的过程}


五 锁的宏初始化 

在上面我们已经简单学习了锁的使用,关于锁的初始化上面用到的是pthread库提供的接口:pthread_mutex_init() ,但是在系统中还存在另一种初始化锁的方法,还方法只针对全局锁进行初始化使用该宏初始化的锁是不需要手动销毁的,即不需要我们调用 pthread_mutex_destroy() 接口

下面演示该宏定义的全局锁的使用:

int tickets=10000;
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;//宏定义的全局锁
void* getTickets(void* args)
{//加锁和解锁是多个线程串行执行的,程序变慢了。std::string username=static_cast<const char*>(args);//ThreadData* td=static_cast<ThreadData*>(args);while(true){ //加锁pthread_mutex_lock(&lock);//加锁直接取地址&lockif(tickets >0){usleep(1244);std::cout<< username<<"我正在抢票"<<tickets<< std::endl;tickets--;   pthread_mutex_unlock(&lock);//解锁// usleep(1244);}else{  pthread_mutex_unlock(&lock);//解锁break;}//抢完票之后,我们设置一个任务。例如形成订单usleep(1000);//形成一个订单给用户return nullptr;
}int main()
{//创建四个线程pthread_t t1, t2, t3, t4;pthread_create(&t1,nullptr,getTickets,(void*)"thread 1");pthread_create(&t2,nullptr,getTickets,(void*)"thread 2");pthread_create(&t3,nullptr,getTickets,(void*)"thread 3");pthread_create(&t4,nullptr,getTickets,(void*)"thread 4");pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);pthread_join(t4,nullptr);//不需要手动销毁锁了即pthread_mutex_destroy(&lock,nullptr) }

测试结果:

接下来我们对几个概念进行再次说明一下:

  • 临界资源:多线程执行流共享的资源(且这个资源是被保护的)就叫做临界资源,例如上文代码的共享资源tickets通过加锁被保护,叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区,例如上文加锁和解锁的中间部分即对tickets进行--的代码区叫做临界区

六 锁的原理

前边说了这么多有关于锁的介绍,那么我们该如何看待锁呢?

1.如何看待锁?

  •  a.  多个线程都能利用锁,即:锁本身就是一个共享资源,既然是共享资源,那么共享资源就要被保护?锁的安全谁来保护呢
  •  b.  锁是共享资源需要被保护,那么加锁这个操作就是原子性的(即要么加锁成功,要么加锁不成功)
  •  c. 加锁如果申请成功,就继续向后执行,如果申请暂时没有成功,执行流会进行阻塞。
  • d. 谁持有锁,谁进入临界区。

如果线程1,申请成功,进入临界资源,正在访问临界资源期间,其他线程只能阻塞等待。

如果线程1,申请成功,进入临界资源,正在访问临界资源期间,那么线程1可以被cpu进行切换吗?答案是可以的,但是当持有锁的线程被切走的时候,即使自己被切走了,其他线程

依然无法继续申锁成功,也便无法继续向后执行,直到线程1释放了锁,其他线程才能申请锁成功,继续往后执行。

2. 如何理解加锁和解锁的本质 

  • 经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,所以保证了原子性,

针对上面的伪代码我们对加锁和解锁进行分析 

首先, al 表示寄存器, mutex 则表示在内存中的锁   (mutex互斥量可以理解为就是一个锁)

movb $0, %al, 把 0 存入 al 寄存器中

xchgb %al, mutex, 交换 al寄存器 和 内存中mutex 的数据

if(al > 0) { return 0; }, 如果 al 寄存器中的数据 大于 0, 则 申请锁成功, 返回 0. 否则, 就阻塞等待.

整个上锁函数执行的语句可以看作这几个过程.  其中, xchgb %al, mutex 操作 是实际上锁的操作.

我们用图来描述, 如果线程1 在执行上锁的操作。

🚲如果没有上锁时, 锁的值是1.那么 执行 xchgb %al, mutex 将 al 中的0 与 mutex 的值交换,

此时 al中的值变为1,这个时候其实以及申锁成功了,如果线程没有被cpu切走,那么

if(al>0)满足,线程就执行后续语句。

al中的值变为1,这个时候表示申锁成功了,但是线程还没往下执行就被cpu切走了,那么后面的线程也不可能申锁成功执行后续代码,这是因为cpu中寄存器对线程进行切换的时候,会把寄存器中关于线程的上下文切走。所以当下一个线程来的时候,寄存器会把新线程的al=0与内存中的mutex=0交换,al的值还是0,不会继续执行,进入阻塞状态,所以此时cpu又会对

上一个线程调度,cpu对线程加载的时候,会把线程上下文重新加载过来,即al=1,所以执行后续代码,当执行完相应的临界区的时候,寄存器再将al=1 与mutex交换,这就是解锁过程,此时mutex=1,后面的线程才有可能申请锁成功,上面的分析说明了加锁和解锁是个二原性行为,保证了共享资源不会被多个线程同时执行,即只能串行运行。


七 c++封装互斥锁

系统调用接口大多采样c接口,c语言是面向过程的,而c++面向对象的,为了更好使用加锁解锁。我们使用c++对互斥锁进行封装。

//Mutex.hpp/
#pragma once 
#include <iostream>
#include <pthread.h>//对锁进行封装,类似undersort_map一样先定义一个结点
//结点成员包含锁,以及加锁解锁,然后在定义了一个类
//类中成员变量是结点,然会类的加锁解锁分别调用结点的成员函数
class Mutex
{
public:Mutex(pthread_mutex_t* lock_p=nullptr):lock_p_(lock_p){}void lock(){if(lock_p_) pthread_mutex_lock(lock_p_);}void unlock(){if(lock_p_) pthread_mutex_unlock(lock_p_);}~Mutex(){}
private:pthread_mutex_t* lock_p_;
};//这才是我们最终想要的封装
class LockGuard
{
public:LockGuard(pthread_mutex_t* mutex):mutex_(mutex){mutex_.lock();//在构造函数中进行加锁}~LockGuard(){mutex_.unlock();//在析构函数中进行解锁}
private:Mutex mutex_;
};

然后我们对测试代码做一下改动

int tickets=10000;
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;//定义一个锁
void* getTickets(void* args)
{//加锁和解锁是多个线程串行执行的,程序变慢了。std::string username=static_cast<const char*>(args);while(true){ {//加了这个{}是相当于加了个作用域,当出了{}解锁成功,后面的usleep()代码没有加锁LockGuard lockguard(&lock);//构造并且加锁,处理作用域自带析构调用解锁函数if(tickets >0){usleep(1244);std::cout<< username<<"我正在抢票"<<tickets<< std::endl;tickets--;   }else{  break;}}//抢完票之后,我们设置一个任务。例如形成订单usleep(1000);//形成一个订单给用户}return nullptr;
}


八 可重入与线程安全 

  •  线程安全:多线程并发运行同一段代码时,并不会影响到整个进程的运行结果,就成为线程安全
  • 可重入同一个函数被不同执行流调用, 在一个执行流执行没结束时, 有其他执行流再次执行此函数, 这个现象叫 重入

1. 可重入与线程安全联系

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

2. 可重入与线程安全区别

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

九 死锁

在多把锁的场景下,我们持有自己的锁不释放,还要对方的锁,对方也是如此,此时就容易造成死锁。自己同时申请多把锁也可能造成死锁。

我们用一个例子进行说明

pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;//定义一个锁
void* getTickets(void* args)
{//加锁和解锁是多个线程串行执行的,程序变慢了。std::string username=static_cast<const char*>(args);//ThreadData* td=static_cast<ThreadData*>(args);while(true){ // //加锁
//**********************************************************************pthread_mutex_lock(&lock);//在这里我们申请了两把锁pthread_mutex_lock(&lock);//在这里我们申请了两把锁
//**********************************************************************if(tickets >0){usleep(1244);std::cout<< username<<"我正在抢票"<<tickets<< std::endl;tickets--;   pthread_mutex_unlock(&lock);//解锁// usleep(1244);}else{  pthread_mutex_unlock(&lock);//解锁break;}//抢完票之后,我们设置一个任务。例如形成订单usleep(1000);//形成一个订单给用户//通过usleep(1000),线程在访问加锁的资源之后,进行休眠即阻塞状态,这个时候cpu会对其他线 程进行随机调度//从而实现了多个进程对保护的共享资源进行抢票的过程}return nullptr;
}

运行结果

🚢:为什么会造成进程卡住的情况呢?首先前面我们说明了锁的原理。 当我们申请了一把锁的时候 pthread_mutex_lock(&lock); 寄存器al 变成1,内存中mutex=0.此时al=1, 满足条件,执行后续语句,然后下一个语句又是申请一把锁  pthread_mutex_lock(&lock),前面的锁没有释放,那么后面的 pthread_mutex_lock(&lock)就会阻塞等待,当cpu切换线程执行其他线程也是会遇到这种情况,那么整个多线程就一直处于阻塞状态,从而不会执行后续cout,和tickets--等语句。导致的结果就是线程一直阻塞,显示器上什么也不显示。

1.死锁产生的必要条件

  1. 互斥条件:: 一个资源每次只能被一个执行流使用
  2. 请求与保持条件: 一个执行流因请求资源(索要锁时)而阻塞时,对已获得的资源(锁)保持不放(锁不释放)
  3. 不剥夺条件: 一个执行流已获得的锁资源,在末使用完之前,不能强行剥夺
  4. 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系(我向你要锁你向我要锁)

2.死锁的避免方法

最直接有效的避免方法是不使用锁. 虽然锁可以解决一些多线程的问题, 但是可能会造成死锁。

如果非要使用锁, 那就得考虑避免死锁,破坏死锁的四个必要条件。

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

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

相关文章

论文阅读笔记——Reactive Diffusion Policy

RDP 论文 通过 AR 提供实时触觉/力反馈&#xff1b;慢速扩散策略&#xff0c;用于预测低频潜在空间中的高层动作分块&#xff1b;快速非对称分词器实现闭环反馈控制。 ACT、 π 0 \pi_0 π0​ 采取了动作分块&#xff0c;在动作分块执行期间处于开环状态&#xff0c;无法及时响…

swagger 注释说明

一、接口注释核心字段 在 Go 的路由处理函数&#xff08;Handler&#xff09;上方添加注释&#xff0c;支持以下常用注解&#xff1a; 注解名称用途说明示例格式Summary接口简要描述Summary 创建用户Description接口详细说明Description 通过用户名和邮箱创建新用户Tags接口分…

STM32 HAL库 OLED驱动实现

一、概述 1.1 OLED 显示屏简介 OLED&#xff08;Organic Light - Emitting Diode&#xff09;即有机发光二极管&#xff0c;与传统的 LCD 显示屏相比&#xff0c;OLED 具有自发光、视角广、响应速度快、对比度高、功耗低等优点。在嵌入式系统中&#xff0c;OLED 显示屏常被用…

Web开发-JavaEE应用动态接口代理原生反序列化危险Invoke重写方法利用链

知识点&#xff1a; 1、安全开发-JavaEE-动态代理&序列化&反序列化 2、安全开发-JavaEE-readObject&toString方法 一、演示案例-WEB开发-JavaEE-动态代理 动态代理 代理模式Java当中最常用的设计模式之一。其特征是代理类与委托类有同样的接口&#xff0c;代理类…

K8s是常用命令和解释

K8s高频命令 获取资源信息&#xff0c;如获取 Pod、Service、Deployment等资源状态信息 kubectl get创建资源如创建Pod、Service、Deployment等资源 kubectl create删除资源&#xff0c;如删除Pod、Service、Deployment等资源 kubectl delete 应用配置文件&#xff0c;如引用D…

【模态分解】EMD-经验模态分解

算法配置页面&#xff0c;也可以一键导出结果数据 报表自定义绘制 获取和下载【PHM学习软件PHM源码】的方式 获取方式&#xff1a;Docshttps://jcn362s9p4t8.feishu.cn/wiki/A0NXwPxY3ie1cGkOy08cru6vnvc

TDengine 语言连接器(Go)

简介 driver-go 是 TDengine 的官方 Go 语言连接器&#xff0c;实现了 Go 语言 database/sql 包的接口。Go 开发人员可以通过它开发存取 TDengine 集群数据的应用软件。 Go 版本兼容性 支持 Go 1.14 及以上版本。 支持的平台 原生连接支持的平台和 TDengine 客户端驱动支持…

链接世界:计算机网络的核心与前沿

计算机网络引言 在数字化时代&#xff0c;计算机网络已经成为我们日常生活和工作中不可或缺的基础设施。从简单的局域网&#xff08;LAN&#xff09;到全球互联网&#xff0c;计算机网络将数以亿计的设备连接在一起&#xff0c;推动了信息交换、资源共享以及全球化的进程。 什…

AI agents系列之全面介绍

随着大型语言模型(LLMs)的出现,人工智能(AI)取得了巨大的飞跃。这些强大的系统彻底改变了自然语言处理,但当它们与代理能力结合时,才真正释放出潜力——能够自主地推理、规划和行动。这就是LLM代理大显身手的地方,它们代表了我们与AI交互以及利用AI的方式的范式转变。 …

如何使用AI辅助开发CSS3 - 通义灵码功能全解析

一、引言 CSS3 作为最新的 CSS 标准&#xff0c;引入了众多新特性&#xff0c;如弹性布局、网格布局等&#xff0c;极大地丰富了网页样式的设计能力。然而&#xff0c;CSS3 的样式规则繁多&#xff0c;记忆所有规则对于开发者来说几乎是不可能的任务。在实际开发中&#xff0c…

复刻系列-星穹铁道 3.2 版本先行展示页

复刻星穹铁道 3.2 版本先行展示页 0. 视频 手搓&#xff5e;星穹铁道&#xff5e;展示页&#xff5e;&#xff5e;&#xff5e; 1. 基本信息 作者: 啊是特嗷桃系列: 复刻系列官方的网站: 《崩坏&#xff1a;星穹铁道》3.2版本「走过安眠地的花丛」专题展示页现已上线复刻的网…

爬虫:IP代理

什么是代理 代理服务器 代理服务器的作用 就是用来转发请求和响应 在爬虫中为何需要使用代理&#xff1f; 有些时候&#xff0c;需要对网站服务器发起高频的请求&#xff0c;网站的服务器会检测到这样的异常现象&#xff0c;则会讲请求对应机器的ip地址加入黑名单&#xff…

协程的原生挂起与恢复机制

目录 &#x1f50d; 一、从开发者视角看协程挂起与恢复 &#x1f9e0; 二、协程挂起和恢复的机制原理&#xff1a;核心关键词 ✅ suspend 函数 ≠ 普通函数 ✅ Continuation&#xff08;协程的控制器&#xff09; &#x1f527; 三、编译器做了什么&#xff1f;&#xff0…

c++11--std::forwaord--完美转发

std::forword的作用 完美转发的核心目的是保持参数的原始类型&#xff08;包括const/volatile限定符和左值/右值性质&#xff09;不变地传递给其他函数。 为什么需要完美转发 在没有完美转发之前&#xff0c;我们面临以下问题&#xff1a; 模板参数传递中的值类别丢失 当参数…

Linux安装开源版MQTT Broker——EMQX服务器环境从零到一的详细搭建教程

零、EMQX各个版本的区别 EMQX各个版本的功能对比详情https://docs.emqx.com/zh/emqx/latest/getting-started/feature-comparison.html

计算机组成原理-存储器

1. 存储器的定义与作用 存储器是计算机系统中用于存储程序、数据和中间结果的硬件设备&#xff0c;是计算机五大核心部件之一。 核心功能&#xff1a; 提供数据的 临时或永久存储 能力。支持CPU按需快速存取指令和数据&#xff0c;是程序运行的物理基础。 2. 存储器的分类 …

单片机领域中哈希表

以下是单片机领域中哈希表的实际应用及编程实例&#xff1a; 1.哈希表在单片机中的实际应用场景 • 命令解析&#xff1a;在单片机通信中&#xff0c;经常需要解析接收到的命令。使用哈希表可以快速地将命令字符串映射到对应的处理函数&#xff0c;提高命令解析的效率。 • 数…

算法思想之位运算(一)

欢迎拜访&#xff1a;雾里看山-CSDN博客 本篇主题&#xff1a;算法思想之位运算(一) 发布时间&#xff1a;2025.4.12 隶属专栏&#xff1a;算法 目录 滑动窗口算法介绍六大基础位运算符常用模板总结 例题位1的个数题目链接题目描述算法思路代码实现 比特位计数题目链接题目描述…

封装Tcp Socket

封装Tcp Socket 0. 前言1. Socket.hpp2. 简单的使用介绍 0. 前言 本文中用到的Log.hpp在笔者的历史文章中都有涉及&#xff0c;这里就不再粘贴源码了&#xff0c;学习地址如下&#xff1a;https://blog.csdn.net/weixin_73870552/article/details/145434855?spm1001.2014.3001…

全星APQP软件:为用户提供高效、合规、便捷的研发管理体验

全星APQP软件&#xff1a;为用户提供高效、合规、便捷的研发管理体验 为什么选择全星APQP软件系统&#xff1f; 在汽车及高端制造行业&#xff0c;研发项目管理涉及APQP&#xff08;先期产品质量策划&#xff09;、FMEA&#xff08;失效模式与影响分析&#xff09;、CP&#x…