当前位置: 首页 > news >正文

多线程(线程互斥)

抢票代码编写

学习了前面有关线程库的操作后,我们就可以模拟抢票的过程
假设我们创建四个线程,分别代表我们的用户
然后设定总票数为1000张,四个线程分别将进行循环抢票操作,其实就是循环对票数进行打印,并进行对应的减减操作
一旦票数为0,也就是票没有了,我们就让线程从循环中退出
当然,我们知道抢票,和抢到票后付费等等操作,都是需要时间的
所以我们每次抢票的时候,加上相应的延时函数usleep,它的功能和sleep函数一样,不过是micro微秒级别的,而sleep里的参数是秒.
在这里插入图片描述
具体的代码实现如下:

1 #include <iostream>2 #include <pthread.h>3 #include <cstring>4 #include <unistd.h>5 using namespace std;6 int tickets = 10000;7 void* BuyTickets(void* args)8 {9   std::string name = static_cast<const char*>(args);10   while (true)11   { 12     if(tickets > 0)13     {14       usleep(2000); //模拟抢票需要的时间15       cout << name << " ticket numbers: " << tickets-- << endl;16     }17     else18     {                                                                                                                                                               19       break;20     }21     //模拟抢到票的后续操作22     usleep(1000);23   }24 25   return nullptr;26 }27 int main()28 {29   pthread_t tids[4];30   int n = sizeof(tids)/sizeof(tids[0]);31   for (int i = 0;i < n;++i)32   {33     char* name = new char[64];34     snprintf(name,64,"thread-%d",i+1);35     pthread_create(tids + i,nullptr,BuyTickets,(void*)name);36   }37 38   for(int i = 0;i < n;++i)39   {40     pthread_join(tids[i],nullptr);41   }42   return 0;43 }

按照我们的预期来说,每个线程都会抢票,当票数抢到0的时候,每个线程都会自动退出循环,停止抢票
但显示打印出来的结果却非常奇怪
可以看到,线程1,3,在tickets数目已经小于等于0的情况下,仍然进去了循环,这是为什么呢?
在这里插入图片描述

问题分析

我们前面提到过每个线程共享的是同一个虚拟地址空间
这也就意味着,**有些资源是每个线程都共享的!**最基本的,比如我们所说的代码段Text Segment,数据段Data Segment都是共享的

一般来说,线程共享的资源有下面几种:

  1. 全局变量
  2. 文件描述符表
  3. 每种信号的处理方式(SIG_ IGN、SIG_DFL或者自定义的信号处理函数)
  4. 当前工作目录
  5. 用户id和组id

可以看到,我们上述的tickets变量就是一个全局变量,是被所有执行流所共享的!这也是我们模拟实现抢票代码的基础
但是上面的结果已经指出,线程中大部分资源直接共享或间接共享,就可能导致我们的并发问题
对于我们编写的一条简单的自增C++代码语句
实际在底层转成汇编代码后,会被转成三条汇编语句进行实现
在这里插入图片描述

第一行汇编语句,我们要先将数据从内存中load到我们的寄存器中,一般是eax
第二行汇编语句,我们要对eax里面load的数据进行相应的加减操作
第三行汇编语句,将寄存器里面的内容,放回到我们数据所在内存的位置

一切看似合情合理,因为只有CPU里面的寄存器配合ALU才有运算能力,不然假如内存可以直接对变量进行加减操作,那我还要CPU干什么?中间商赚差价吗?
但是问题的出现,也正是这个原因
线程的切换,是随时都有可能发生的
假如存在两个线程A,B,当线程A执行的时候,刚好把tickets减为0,运行到对应的第二行代码,突然操作系统OS大哥说:“你工作时间到了,该要线程B工作了!”,线程A只能带着它的上下文和对应减为0的tickets变量,灰溜溜的走了
但是,注意此时线程A有执行第三行汇编代码吗?
没有!线程B眼中的数据tickets,还是等于1
这就意味着线程B对于if语句的判断,依旧是成立的!
所以线程B仍然会进来
但是操作系统OS大哥又有点不太满意了,说:“线程B你的动作太慢了,还是先让线程A把剩下的活先干完吧,等等再分时间给你”
于是线程A就把tickets == 0的数据加载到内存中(继续运行第三行汇编代码)
那此时再切换回线程B的话,在线程B的眼里,tickets此时等于什么呢?
答案是0
但是我已经过了if那条判断了,因此放到寄存器里进行加减操作,会得到-1!这就是-1的由来

抽离概念

解决一个问题的前提,是先描述准确这个问题
而描述问题,无法避免的就是要引入一些概念和定义
我们把上述不同线程看到的同一份共享资源,我们称作为临界资源
临界资源在任何时刻,都只允许一个执行流进行访问
而访问临界资源的这部分代码,我们称之为临界区;反之不访问的话,我们就称作为非临界区
最后我们在定义一个概念,我们称之为原子性
原子性指的是不可被分割的操作,该操作不会被任何调度机制打断,该操作只有两态,要么完成,要么未完成;就像我们高中老师经常说的一句话,你可以不做,要做,就一定要完成
有了这三个概念,我们就可以准确的对上面的问题进行描述了
1.上述的问题,只会发生在临界区中,非临界区中,并不存在访问临界资源的概念
2.问题的出现,正是由于我们所看的一句代码,在底层汇编中,等价于三行代码语句,并不是原子性的!
在这里插入图片描述

解决问题

锁的引入

描述完问题后,我们就要着手解决这个问题
而在linux操作系统中,大佬早就给我们想好解决方案
答案就是加锁
在原生线程库中,已经设计好一种名为**锁(互斥量)**的结构,专门用来解决类似问题
在这里插入图片描述
其中上述两个函数是搭配使用,初始化init,和销毁destroy
(没错和指针类似操作,有创建,用完后,记得及时销毁)
而如果锁是一个静态或者全局变量,按下面的方式进行初始化,则不用销毁,操作系统OS会帮你自动销毁
只要在对应的临界区加锁,解锁,我们就可以解决多线程并发的问题
对应的加锁,解锁函数,分别叫做
pthread_mutex_lock()与pthread_mutex_unlock()

PS:互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁

参数都只有一个,就是指向我们锁的指针
在这里插入图片描述
下面,我们简单来编写一段代码体验一下加锁操作

  1 #include <iostream>2 #include <pthread.h>3 #include <cstring>4 #include <unistd.h>5 using namespace std;6 int tickets = 1000;7 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;8 void* BuyTickets(void* args)9 {10    std::string name = static_cast<const char*>(args);11    while (true)12    {  13       pthread_mutex_lock(&mutex);  //临界区加锁14       if(tickets > 0)15       {16         usleep(2000); //模拟抢票需要的时间17         cout << name << " ticket numbers: " << tickets-- << endl;18         pthread_mutex_unlock(&mutex); //临界区结束及时解锁19       }20       else21       {22         pthread_mutex_unlock(&mutex); //在循环结束break时,也要记得解锁23         break;24       }                                                                                                                                                             25       //模拟抢到票的后续操作26       usleep(1000);27     }28 29   return nullptr;30 }31 int main()32 {33     pthread_t tids[4];34     int n = sizeof(tids)/sizeof(tids[0]);35     for (int i = 0;i < n;++i)36     {37       char* name = new char[64];38       snprintf(name,64,"thread-%d",i+1);39       pthread_create(tids + i,nullptr,BuyTickets,(void*)name);40     }41     for (int i = 0;i < n;++i)42     {43       pthread_join(tids[i],nullptr);44     }45    return 0;46 }47

可以看到加锁后,结果就完美符合我们的预期了
票数不会再出现减到-1,-2的情况
在这里插入图片描述

改造锁代码

但是上面的写法,显然非常简单
用C++代码实现,那我们肯定也要试一下封装来实现锁
我们构建一个TData类,其中里面包括线程的名字,还有对应的锁指针

class TData
{
public:TData(const string& name,pthread_mutex_t*mutex):_name(name),_pmutex(mutex){}~TData(){}
public:string _name; //线程对应的名字pthread_mutex_t* _pmutex;
};

则上述代码可以改造成这样
这里我们锁并没有设成全局变量或静态变量,而是采用了第一种方式创建,调用init,destroy函数

  1 #include <iostream>2 #include <pthread.h>3 #include <cstring>4 #include <unistd.h>5 using namespace std;6 int tickets = 1000;7 //pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;8 class TData9 {10 public:11   TData(const string& name,pthread_mutex_t* mutex):_name(name),_pmutex(mutex)12   {}13   ~TData()14   {}15 public:16   string _name; //线程对应的名字17   pthread_mutex_t* _pmutex;18 };19 void* BuyTickets(void* args)20 {21    TData* td = static_cast<TData*>(args);22    while (true)23    {24       pthread_mutex_lock(td->_pmutex);25       if(tickets > 0)26       {27         usleep(2000); //模拟抢票需要的时间28         cout << td->_name << " ticket numbers: " << tickets-- << endl;29         pthread_mutex_unlock(td->_pmutex);30       }                                                                                                                                                             31       else32       {33         pthread_mutex_unlock(td->_pmutex); 34         break;35       }36       //模拟抢到票的后续操作                                                                                                                                        37       usleep(1000);38     }39   40   return nullptr;41 }42 int main()43 {44     pthread_t tids[4];45     pthread_mutex_t mutex;46     pthread_mutex_init(&mutex,nullptr); //对锁进行初始化,第二个参数为所得属性,一般设为nullptr47     int n = sizeof(tids)/sizeof(tids[0]);48     for (int i = 0;i < n;++i)49     {50       char* name = new char[64];51       snprintf(name,64,"thread-%d",i+1);52       TData* td = new TData(name,&mutex);53       pthread_create(tids + i,nullptr,BuyTickets,td);54     }55     for (int i = 0;i < n;++i)56     {57       pthread_join(tids[i],nullptr);58     }59    pthread_mutex_unlock(&mutex); //销毁锁60    return 0;61 }

可以看到结果和我们的预期说一样的,和之前也是相同的
在这里插入图片描述

为什么叫锁呢?

回答这个问题其实很简单,像我们学校总有一些教室
我们称之为公共资源
一间教室,一次肯定只能够一个社团举办活动(除了几个社团联合举办活动等特殊情况外)
但是我们如何能够做到一间教室只能够一个社团使用呢?
假如其他人闯进来强行霸占呢?
这个时候,就需要给教室的门加一把锁
我使用教室的时候(访问临界资源时),别人能够从门里面进来吗?
答案是不能,这也就解决了多个社团(线程)访问同一间教室(临界资源),造成并发问题出现的可能

细节剖析

1.凡是访问同一个临界资源的线程,都要进行加锁保护,并且这一把锁要是同一把锁,这是一个游戏规则,不能有例外! 你不能说同时上两把不一样的锁;或者有部分线程没上锁,一部分线程上锁等情况出现
2.每一个线程访问临界区时都得加锁,加锁后,能够使原本并行执行的代码,转变为串行执行 但这也同样意味着效率下降,因此,我们可以看到运行时间明显提高了不少 串行化的代价无法解决,但可以减弱
那就是加锁的粒度尽量细一点,我们加锁的时候,只给对应的代码加锁,临界区不需要很大!

那临界区可不可以是一行代码呢?答案是可以的!
临界区可以是一行代码,也可以是一批代码,取决于我们哪部分代码访问了临界资源
还有一个常见的误区,在加锁后,线程可以被切换吗?
很多人可能都会回答不可以,而答案恰恰相反,是可以被切换的!
对于加锁和解锁,我们并不需要特殊化它们,在计算机眼里,它们也仅仅是一批普通代码
这就类似于我们大学里面可能会有一个人的VIP自习室
一次只能供一个人预约,一旦有人预约了,只有他自己从系统选择退出,才能有新的人预约,是一个道理
预约了自习室的人,可以没有在自习室里面自习,去吃饭了(没有工作,此时其它线程被OS调度)
但是没有影响!!!其它人进不去自习室里面,因为系统上还显示我预约占领着自习室
这也正体现互斥带来串行化的表现,站在其它线程角度,只有两种状态,锁被我申请了(持有锁),锁被我释放了(不持有锁)

锁的原理

于是就有人质疑了
你说不同线程看到的都是同一把锁,也就意味着锁本身就是公共资源
那锁如何保证自己的安全呢?为什么加锁就能解决并发问题呢?
关键就在于我们前面提到过的原子性
加锁和解锁这个动作都是原子性的,它可和我们的加减操作不同,也就是,只要进行加锁操作,谁都无法打断我,我一定会成功完成!否则就是失败,不存在苟且偷生(做一半),只有破釜沉舟

预备知识1

在大多数体系结构都提供了swap或exchange(XCHG)指令,拿XCHG指令来说,它相当于MOV指令的简化版,但它其中有一个强大的功能,就是把寄存器和内存单元的数据相交换
这是一条指令,换句话说,这条指令基础保证了我们原子性的实现的可能

预备知识2

我们需要意识到寄存器硬件只有一套的,用于临时存储和操作数据,以便在指令级别上执行各种操作
但我们现在是多个线程
这也就意味着这一套寄存器,必定由多个线程所共用
但寄存器又如何区分这多个线程呢?
答案就是每个线程都有自己的寄存器上下文.
当操作系统进行线程上下文切换时,它会保存当前线程的寄存器上下文,然后加载下一个线程的寄存器上下文.
这就意味着每个线程可以独立地使用一组寄存器来执行其指令和操作数据,而不会与其他线程干扰.
这种隔离保证了线程之间的相互独立性
但是寄存器内部的数据,是每一个线程都有的!
就好比图书馆的课桌还有电脑插头,这些都是只有一套的(寄存器只有一套)
但是我们每个人使用它的时候,放置的书本,水壶等等,往往是不同的
(线程之间具有相互独立性)
但是假如有一天在图书馆的课桌上放了一张纸,上面写道:“该课桌要维修,临时不能使用”,则我们每个线程想使用该桌子时,都会看到这个内容,然后自觉离去(寄存器里的内容是每个线程都有的)
换句话说,寄存器不能简单把它等同于寄存器的内容,它只是一个临时存储和操作数据的硬件,对于每一个线程而言,寄存器里的数据+线程自己独有的寄存器上下文,这才构成了线程所拥有的内容

原理讲解

因此,假如我们把mutex,这一把锁,简单看作1
在底层,lock的伪代码是这样实现的
在这里插入图片描述
第一句指令将寄存器al里面的值清0,换言之,对于每个线程来说,每个线程执行这段代码,实际上就是向自己的上下文写0
第二句指令,就是将内存中的mutex与寄存器al里面的值进行交换(XCHG指令),并且该指令操作,是原子性的!只有一条代码
这句指令执行外后,会出现什么情况?
有且只有一个线程顺利得到锁,它上下文的内容会变成1
但是其它线程呢?
只有它上下文的0和内存里面的0进行互换
(不会新增任何的1,而1只会进行流转)
第三条指令判断al寄存器里的内容是否大于0,不是则被挂起
最后的结果也就显而易见了
即便当前有锁的线程被切走,但是其它线程你没有锁啊!对应我们之前的故事,就是没有预约VIP自习室啊!那就算校长来了都没有用,门不会给你打开

交换的本质: 将共享数据交换到自己私有上下文中

这就是加锁的原理,一个不让你通过的策略,来实现在临界区,由并发执行,转为串行执行

那解锁呢?
就是将mutex里面的内容置1,把预约取消,锁放回去的过程 那没有把al寄存器里的内容置0,会不会有什么影响?
反之加锁的第一步,又会全部清0,所以完全不用担心

demo版的线程封装

在了解互斥量后,我们可以尝试对线程进行封装
创建一个Thread.hpp文件

类内成员设计

设计一个类,首先我们设定类内成员是什么
既然是线程封装,那线程id肯定要有吧
那不同线程,肯定会有对应的线程名字,所以也可以加个string类型的name
然后每个线程,也会有对应的运行状态,因此还可以加一个status
在创建的时候,我们还可以先把指定的函数给定,因此还可以加上两个参数,func与args
其中func为线程运行的函数,而args则是一个空指针
在这里插入图片描述
在这里插入图片描述

构造与析构

我们先指定线程id初始化为0,初始运行状态为新线程
构造的时候,只要传对应线程的号码,用来初始化名字
还有传入对应线程运行的函数,以及对应的args参数即可
在这里插入图片描述

类内方法

没有什么好说的,整体就是返回类内的属性,使得我们用户以后创建对象后,能够迅速调用对象的属性进行查看
在这里插入图片描述

线程运行

线程运行,实际上就是进行真正意义上,线程的创建
也就是我们在我们的Run函数中要调用pthread_create函数
其中的第三个参数,是我们之前所提到过用户传进来的参数
但是,我们今天,不直接传进去_func与_args参数
而是换种方法,在类内部实现一个函数,让我们调用pthread_create函数时,调用该函数
在这里插入图片描述
但是程序此时会发生报错
这是因为在类内部的函数,第一个参数实际上会隐含this指针,指向该对象
因此,调用该函数的时候,由于参数不匹配,pthread_create要求的函数的参数只能有void*.
所以我们把它设为静态static函数
但是这又会引发一个新的问题
虽然参数里面没有this指针了,但是静态函数,就不能再访问类内成员了!
这里提供一个解决办法:
调用pthread_create函数时,将对象的this指针传进来
这样运行的时候,只需要对this指针解引用,就可以得到该对象,那就可以访问对象里面的属性了
在这里插入图片描述

线程等待

在这里插入图片描述

整体代码

    1 #include <iostream>2 #include <stdlib.h>3 #include <pthread.h>4 #include <cstring>5 #include <string>6 class Thread{7 public:8   typedef enum9   {10      NEW = 0,11      RUNNING,12      EXITED13   }ThreadStatus;14     typedef void* (*func_t)(void*);15 public:16   Thread(int num,func_t func,void* args):_tid(0),_status(NEW),_func(func),_args(args)17   {18      //名字由于还要接收用户给的编号,因此在构造函数内进行初始化19      char buffer[128];                                                                                                                                            20      snprintf(buffer,sizeof(buffer),"thread-%d",num);21      _name = buffer;22   }23   ~Thread()24   {}25   //返回线程的状态26   int status()  {return _status;}27   //返回线程的名字28   std::string name() {return _name;}29   //返回线程的id30   //只有线程在运行的时候,才会有对应的线程id31   pthread_t GetTid()32   {33     if (_status == RUNNING)34     {35       return _tid;36     }                                                                                                                                                             37     else38     {39       return 0;40     }41   }42   //类成员函数具有默认参数this43   //但是会有新的问题44   static void * ThreadRun(void* args)45   {46     Thread* ts = (Thread*)args;  //此时就获取到我们对象的指针47     // _func(args);  //此时就无法回调相应的方法(成员函数无法直接被访问)48     (*ts)();49     return nullptr;50   }51   void operator()() //仿函数52   {53      //假如传进来的线程函数不为空,则调用相应的函数54      if(_func != nullptr)  _func(_args);55   }56   //线程运行57   void Run()58   {59     //线程创建的参数有四个60     //int n = pthread_create(&_tid,nullptr,_func,_args);61     int n = pthread_create(&_tid,nullptr,ThreadRun,this);62     if(n != 0)  exit(0);63     _status = RUNNING;64   }65 66   //线程等待67   void Join()68   {69     int n = pthread_join(_tid,nullptr);                                                                                                                           70     if (n != 0)71     {72        std::cerr << "main thread join error :" << _name << std::endl;73        return;74     }75     _status = EXITED;76   }77 private:78    pthread_t _tid;    //线程id79    std::string _name; //线程的名字80    func_t _func;       //未来要回调的函数81    void*_args;82    ThreadStatus _status; //目前该线程的状态83 };

代码测试

int main()
{Thread t1(1,threadRun,(void*)"Hello!");Thread t2(2,threadRun,(void*)"Hello!");cout << "thread name: " << t1.name() << " thread id: " << t1.GetTid() << " Thread status: "<< t1.status() << endl;cout << "thread name: " << t2.name() << " thread id: " << t2.GetTid() << " Thread status: "<< t2.status() << endl;t1.Run();t2.Run();cout << "thread name: " << t1.name() << " thread id: " << t1.GetTid() << " Thread status: "<< t1.status() << endl;cout << "thread name: " << t2.name() << " thread id: " << t2.GetTid() << " Thread status: "<< t2.status() << endl;t1.Join();t2.Join();cout << "thread name: " << t1.name() << " thread id: " << t1.GetTid() << " Thread status: "<< t1.status() << endl;cout << "thread name: " << t2.name() << " thread id: " << t2.GetTid() << " Thread status: "<< t2.status() << endl;                                              return 0;
}

运行结果如下:
在这里插入图片描述
当成功实现线程封装,相信我们对C++多线程库的封装,也就有了更深一步的理解

demo版的锁的封装

前面我们提到过,创建一个锁,既要考虑lock,又要考虑unlock问题
那我们能不能封装锁,使其变成一个LockGuard类,能够自动加锁,解锁呢?
答案是可以的!
只要在其创建的时候,调用我们的封装的锁的Lock类方法
析构的时候,调用我们封装的锁的Unlock方法即可实现

  1 #pragma once2 3 #include <iostream>4 #include <pthread.h>5 6 class Mutex7 {8 public:9   Mutex(pthread_mutex_t* mutex):pmutex(mutex)10   {}11   ~Mutex()12   {}13   void Lock()14   {15      pthread_mutex_lock(pmutex);16   }17   void Unlock()18   {19     pthread_mutex_unlock(pmutex);20   }21 private:22    pthread_mutex_t* pmutex;23 };24 25 class LockGuard26 {27 public:28    LockGuard(pthread_mutex_t* mutex):_mutex(mutex)29    {30      //在创建的时候,就自动上锁                                                                                                                                     31      _mutex.Lock();32    }33    ~LockGuard()34    {35      //销毁的时候,自动解锁36      _mutex.Unlock();37    }38 39 private:40   Mutex _mutex;41 };

这样以后,我们编写代码,将会变得更为优雅
像我们之前实现的抢票代码,只需要在临界区前创建一个对象
然后用一个花括号将临界区括起来,表示其为临界区
则自动加锁,解锁,解决并发问题
在这里插入图片描述

线程安全和函数重入

首先要意识到
两者谈论的不是一个维度的东西,只能说两者有重叠的部分,但并非是简单的包含与非包含的关系
线程安全是我们必须要保证的!
但大部分函数其实都是不可重入的,因此函数重入并没有好坏之分!仅仅是函数的特征

不可重入函数只是有可能引发线程安全问题,我线程调用的时候不访问全局变量/静态变量,注重各种细节,那就不会引发线程安全问题

常见线程不安全的情况:
1.不保护共享变量的函数
比如我们上述最开始实现的抢票函数
2.函数状态随着被调用,状态发生变化的函数
比如static修饰的静态的函数,每次调用可能都会引发相应状态的改变
3.返回指向静态变量指针的函数
4.调用线程不安全函数的函数

常见线程安全的情况:
1.每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
2.类或者接口对于线程来说都是原子操作(比如说加锁)
3.多个线程之间的切换不会导致该接口的执行结果存在二义性

相关文章:

多线程(线程互斥)

抢票代码编写 学习了前面有关线程库的操作后&#xff0c;我们就可以模拟抢票的过程 假设我们创建四个线程&#xff0c;分别代表我们的用户 然后设定总票数为1000张&#xff0c;四个线程分别将进行循环抢票操作&#xff0c;其实就是循环对票数进行打印&#xff0c;并进行对应的…...

使用 html2canvas 和 jspdf 将页面转 pdf,同时解决当页面过长时,页面白屏问题

代码如下&#xff0c;直接粘贴复制即可&#xff0c;代码中 jspdf 是全局引入&#xff0c;你可以自己局部引入 别人使用标签的方式来显示 base64&#xff0c;但是当页面过长时&#xff0c;base64 大小过大会导致页面解析异常&#xff0c;显示白屏 import html2canvas from html2…...

【Python 千题 —— 基础篇】今年几岁啦

题目描述 题目描述 介绍自己的年龄。请使用 input 函数读入一个整数&#xff0c;表示自己的年龄&#xff0c;然后程序将自动生成介绍自己年龄的英文语句。 输入描述 输入一个整数&#xff0c;表示自己的年龄。 输出描述 程序将生成一个英文语句&#xff0c;以介绍自己的年…...

git push 失败 shallow update not allowed

问题描述&#xff1a; ~/OK62xx-linux-sdk/OK62xx-linux-kernel$ git push origin master Counting objects: 83919, done. Delta compression using up to 144 threads. Compressing objects: 100% (75697/75697), done. Writing objects: 100% (83919/83919), 232.41 MiB | …...

uniapp 在uni.scss 根据@mixin定义方法 、通过@include全局使用

在官方文档中提及到uni.scss中变量的使用&#xff0c;而我想定义方法&#xff0c;这样写css样式更方便 一、官方文档的介绍 根据官方文档我知道&#xff0c;在这面定义的变量全局都可使用。接下来我要在这里定义方法。 二、在uni.scss文件中定义方法 我在uni.scss文件中定义了…...

C++ 类和对象(一)

1.面向过程和面向对象初步认识 C语言是面向过程的&#xff0c;关注的是过程&#xff0c;分析出求解问题的步骤&#xff0c;通过函数调用逐步解决问题。 C是基于面向对象的&#xff0c;关注的是对象&#xff0c;将一件事情拆分成不同的对象&#xff0c;靠对象之间的交互完 成。 …...

rust函数

一 、函数定义 &#xff08;一&#xff09;格式 使用fn关键字 fn是 function 的缩写 1.无返回值的 fn 函数名 ( [paraname: type]... ) {函数体 }参数必须声明参数名称和类型 函数定义时指定的参数叫做 形参。调用时传递给函数的参数叫做 实参。 例子 fn another_function(…...

链表的基本操作

&#xff08;一&#xff09;实验类型&#xff1a;设计性 &#xff08;二&#xff09;实验目的&#xff1a; 1. 掌握线性表的链式存贮结构及基本操作&#xff0c;深入了解链表的基本特性&#xff0c;以便在实际问题背景下灵活运用它们。 2. 巩固该存贮结构的构造方法&#xff0…...

Flutter AI五子棋

前言 在上一篇文章中&#xff0c;讲解了如何实现双人在本地对战的五子棋&#xff0c;但是只有一个人的时候就不太好玩&#xff0c;同时博主也没有把五子棋相关的文章写过瘾。那么这篇文章&#xff0c;我们来实现一个功能更加丰富的五子棋吧&#xff01;在设计五子棋的算法方面&…...

springboot项目中后台文件上传处理

参考地址:http://www.gxcode.top/code 文件上次核心处理代码: @Autowired private FileUpload fileUpload; //获取资源对象:file-upload-prod.properties@ApiOperation(value = "用户头像修改", notes = "用户头像修改", httpMethod =...

【SQL】MySQL中的存储引擎、事务、锁、日志

存储引擎&#xff1a; 数据库管理系统&#xff08;DBMS&#xff09;使用数据存储引擎进行创建、查询、更新和删除数据。 MySQL5.5之前默认的存储引擎是MyISAM&#xff0c;5.5及之后版本默认的存储引擎是InnoDB。(my.ini中指定的) MyISAM&#xff1a;不支持事务&#xff0c;不支…...

DRM全解析 —— CRTC详解(2)

接前一篇文章:DRM全解析 —— CRTC详解(1) 本文继续对DRM中CRTC的核心结构struct drm_crtc的成员进行释义。 3. drm_crtc结构释义 (5)struct drm_modeset_lock mutex /*** @mutex:** This provides a read lock for the overall CRTC state (mode, dpms* state, ...) an…...

3d环形图开发(vue3+vite+ts)

开发效果&#xff08;待完善&#xff09;&#xff1a; 技术支持&#xff1a; Echarts echarts-gl 安装&#xff1a; 注&#xff1a;echarts与echarts-gl版本需对应&#xff0c;可参考官网 pnpm add echarts4.9.0 echarts-gl1.1.2 组件封装&#xff1a; <template><…...

element ui中父子组件共用一个el-dialog弹窗,切换组件页面弹窗进行关闭

在Element UI中&#xff0c;如果多个父子组件共用一个el-dialog弹窗&#xff0c;并且需要在切换组件页面时关闭弹窗&#xff0c;你可以考虑以下方法来实现&#xff1a; 使用Vuex进行状态管理&#xff1a; 在Vuex中创建一个状态来管理弹窗的显示状态&#xff08;例如&#xff0…...

基于Keil a51汇编 —— Segments, Modules, and Programs

段、模块和程序 在初始设计阶段&#xff0c;定义程序要执行的任务&#xff0c;然后划分为子程序。以下是与 Ax51 汇编器和 Lx51 链接器/定位器一起使用的子程序类型的简要介绍。 段是代码块或数据存储器。段可以是可重定位的&#xff0c;也可以是绝对的。可重定位段具有名称、…...

基于Java+SpringBoot+Vue民宿管理系统的设计与实现 前后端分离【Java毕业设计·文档报告·代码讲解·安装调试】

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…...

【Qt】三种方式实现抽奖小游戏

简介 本文章是基本Qt与C实现一个抽奖小游戏&#xff0c;用到的知识点在此前发布的几篇文章。 下面是跳转链接&#xff1a; 【Qt控件之QLabel】用法及技巧链接&#xff1a; https://blog.csdn.net/MrHHHHHH/article/details/133691441?spm1001.2014.3001.5501 【Qt控件之QPus…...

【算法与数据结构】--算法基础--算法设计与分析

一、贪心算法 贪心算法是一种解决优化问题的算法设计方法&#xff0c;其核心思想是在每一步选择当前状态下的最优解&#xff0c;从而希望最终达到全局最优解。下面将介绍贪心算法的原理、实现步骤&#xff0c;并提供C#和Java的实现示例。 1.1 原理&#xff1a; 贪心算法的原…...

vue部分入门知识点代码示例

1. Vue实例 Vue.js的核心是Vue实例&#xff0c;用来管理你的应用。以下是一个创建Vue实例的示例&#xff1a; <!DOCTYPE html> <html> <head><title>Vue.js入门示例</title><!-- 引入Vue.js库 --><script src"https://cdn.jsdel…...

【图灵】Spring为什么要用三级缓存解决循环依赖问题

这里写自定义目录标题 一、什么是循环依赖二、什么是单例池&#xff1f;什么是一级缓存&#xff1f;三、什么是二级缓存&#xff0c;它的作用是什么&#xff1f;四、什么是三级缓存&#xff0c;它的作用是什么&#xff1f;五、为什么Spring一定要使用三级缓存来解决循环依赖六、…...

synchronized 学习

学习源&#xff1a; https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖&#xff0c;也要考虑性能问题&#xff08;场景&#xff09; 2.常见面试问题&#xff1a; sync出…...

条件运算符

C中的三目运算符&#xff08;也称条件运算符&#xff0c;英文&#xff1a;ternary operator&#xff09;是一种简洁的条件选择语句&#xff0c;语法如下&#xff1a; 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true&#xff0c;则整个表达式的结果为“表达式1”…...

el-switch文字内置

el-switch文字内置 效果 vue <div style"color:#ffffff;font-size:14px;float:left;margin-bottom:5px;margin-right:5px;">自动加载</div> <el-switch v-model"value" active-color"#3E99FB" inactive-color"#DCDFE6"…...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…...

Linux-07 ubuntu 的 chrome 启动不了

文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了&#xff0c;报错如下四、启动不了&#xff0c;解决如下 总结 问题原因 在应用中可以看到chrome&#xff0c;但是打不开(说明&#xff1a;原来的ubuntu系统出问题了&#xff0c;这个是备用的硬盘&a…...

技术栈RabbitMq的介绍和使用

目录 1. 什么是消息队列&#xff1f;2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...

使用Spring AI和MCP协议构建图片搜索服务

目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式&#xff08;本地调用&#xff09; SSE模式&#xff08;远程调用&#xff09; 4. 注册工具提…...

第7篇:中间件全链路监控与 SQL 性能分析实践

7.1 章节导读 在构建数据库中间件的过程中&#xff0c;可观测性 和 性能分析 是保障系统稳定性与可维护性的核心能力。 特别是在复杂分布式场景中&#xff0c;必须做到&#xff1a; &#x1f50d; 追踪每一条 SQL 的生命周期&#xff08;从入口到数据库执行&#xff09;&#…...

Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?

Pod IP 的本质与特性 Pod IP 的定位 纯端点地址&#xff1a;Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址&#xff08;如 10.244.1.2&#xff09;无特殊名称&#xff1a;在 Kubernetes 中&#xff0c;它通常被称为 “Pod IP” 或 “容器 IP”生命周期&#xff1a;与 Pod …...

篇章二 论坛系统——系统设计

目录 2.系统设计 2.1 技术选型 2.2 设计数据库结构 2.2.1 数据库实体 1. 数据库设计 1.1 数据库名: forum db 1.2 表的设计 1.3 编写SQL 2.系统设计 2.1 技术选型 2.2 设计数据库结构 2.2.1 数据库实体 通过需求分析获得概念类并结合业务实现过程中的技术需要&#x…...