多线程(线程互斥)

抢票代码编写

学习了前面有关线程库的操作后,我们就可以模拟抢票的过程
假设我们创建四个线程,分别代表我们的用户
然后设定总票数为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.多个线程之间的切换不会导致该接口的执行结果存在二义性

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

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

相关文章

强化学习问题(二)--- ERROR: Failed building wheel for box2d-py

错误&#xff1a;Could not build wheels for box2d-py, which is required to install pyproject.toml-based projects pyproject.toml-based projects&#xff1a;意思是缺少依赖包&#xff0c;对于box2d就是缺少swig 注意&#xff1a;安装python对应的swig版本 解决1&…

Linux线程安全

线程安全 Linux线程互斥进程线程间的互斥相关背景概念互斥量mutex互斥量的接口互斥量实现原理探究 可重入VS线程安全概念常见的线程不安全的情况常见的线程安全的情况常见的不可重入的情况常见的可重入的情况可重入与线程安全联系可重入与线程安全区别 常见锁概念死锁死锁的四个…

Unity 捕鱼游戏开发教程与源码

效果图展示 项目分析 主要功能点&#xff1a; 鱼的移动路线 这里使用简单移动的方式&#xff1a;随机位置然后随机鱼直线或者每帧更新鱼的角度实现走圆形。枪随着鼠标或点击位置移动 这个用坐标转换参考代码 private void Update(){Vector3 mousePos; // 鼠标位置// RectTra…

牛津大学海外学习:14天的知识与文化之旅

牛津——一个充满学术氛围与古老传统的城市&#xff0c;对于我这次14天的海外学习经验来说&#xff0c;这里每一个角落都隐藏着知识和历史的故事。作为中国的一名学生&#xff0c;能够在这里学习、生活&#xff0c;真是一次难得的机会。 我报名的是《人工智能》课程&#xff0…

ElasticSearch 学习7 集成ik分词器

网上找了一大堆&#xff0c;很多都介绍的不详细&#xff0c;开始安装完一直报错找不到plugin-descriptor.properties&#xff0c;有些懵这个东西不应该带在里面吗&#xff0c;参考了一篇博客说新建一个这个&#xff0c;新建完可以启动&#xff0c;但是插入索引数据会报错找不到…

Step2:Java内存区域与内存溢出异常

文章目录 1.1 概述1. 2 运行时数据区域1. 3 HotSpot虚拟机对象探秘1. 4 作业:OutOfMemoryError异常体验1.1 概述 对于Java程序员来说,再虚拟机自动内存管理机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄露和内存溢出的问题,看起来由虚…

【广州华锐互动】AR轨道交通综合教学平台的应用

轨道交通是一种复杂且精密的系统&#xff0c;涵盖了众多技术和工程学科&#xff0c;包括机械、电气和计算机科学等。对于学生来说&#xff0c;理解和掌握这些知识是一项挑战。然而&#xff0c;AR技术的出现为解决这一问题提供了可能。 通过AR技术&#xff0c;教师可以创建生动、…

Typescript 综合笔记:解读一个github中的React 网页

1 repository来源和效果 zhitern/ntu-scse22-0163-web (github.com) 2 核心代码异同&#xff08;相比于初始创建的代码&#xff09; 2.1 index.html 和初始创建的是一样的 2.2 App.css 和初始创建的是一样的 2.3 index.tsx 唯一”不一样“的是紫色部分,tsx文件中多了一个…

【附代码】使用Shapely计算多边形外扩与收缩

文章目录 相关文献效果图代码 作者&#xff1a;小猪快跑 基础数学&计算数学&#xff0c;从事优化领域5年&#xff0c;主要研究方向&#xff1a;MIP求解器、整数规划、随机规划、智能优化算法 本文档介绍如何使用 Shapely Python 包 计算多边形外扩与收缩。 如有错误&…

C中volatile总结

在CPU处理过程中&#xff0c;需要将内存中的数据载入到寄存器中才能计算&#xff0c;所以可能涉及到一个问题&#xff0c;如果内存中的数据被更改了&#xff0c;但是寄存器还是使用的旧数据&#xff0c;这样就会造成数据的不同步。 一、volatile关键字的作用 使用volatile关键…

如何在Windows系统搭建VisualSVN服务并在公网远程访问【内网穿透】

文章目录 前言1. VisualSVN安装与配置2. VisualSVN Server管理界面配置3. 安装cpolar内网穿透3.1 注册账号3.2 下载cpolar客户端3.3 登录cpolar web ui管理界面3.4 创建公网地址 4. 固定公网地址访问 前言 SVN 是 subversion 的缩写&#xff0c;是一个开放源代码的版本控制系统…

【WebService】C#搭建的标准WebService接口,在使ESB模版作为参数无法获取参数数据

一、问题说明 1.1 问题描述 使用C# 搭建WebService接口&#xff0c;并按照ESB平台人员的要求&#xff0c;将命名空间改为"http://esb.webservice",使用PostmanESB平台人员提供的入参示例进行测试时&#xff0c;callBussiness接口参数message始终为null。 以下是ES…

【LeetCode】——链式二叉树经典OJ题详解

主页点击直达&#xff1a;个人主页 我的小仓库&#xff1a;代码仓库 C语言偷着笑&#xff1a;C语言专栏 数据结构挨打小记&#xff1a;初阶数据结构专栏 Linux被操作记&#xff1a;Linux专栏 LeetCode刷题掉发记&#xff1a;LeetCode刷题 算法头疼记&#xff1a;算法专栏…

【C++】笔试训练(四)

目录 一、选择题二、编程题1、计算糖果2、进制转换 一、选择题 1、有以下程序&#xff0c;程序运行后的输出结果是&#xff08;&#xff09; #include<iostream> #include<cstdio> using namespace std; int main() {int m 0123, n 123;printf("%o %o\n&…

【数据结构】栈和队列-- OJ

目录 一 用队列实现栈 二 用栈实现队列 三 设计循环队列 四 有效的括号 一 用队列实现栈 225. 用队列实现栈 - 力扣&#xff08;LeetCode&#xff09; typedef int QDataType; typedef struct QueueNode {struct QueueNode* next;QDataType data; }QNode;typedef struct …

IntelliJ IDEA失焦自动重启服务的解决方法

IDEA 热部署特性 热部署&#xff0c;即应用正属于运行状态时&#xff0c;我们对应用源码进行了修改更新&#xff0c;在不重新启动应用的情况下&#xff0c;可以能够自动的把更新的内容重新进行编译并部署到服务器上&#xff0c;使修改立即生效。 现象 在使用 IntelliJ IDEA运…

2023品牌新媒体矩阵营销洞察报告:流量内卷下,如何寻找增长新引擎?

近年来&#xff0c;随着移动互联网的发展渗透&#xff0c;短视频、直播的兴起&#xff0c;新消费/新零售、兴趣电商/社交电商等的驱动下&#xff0c;布局线上渠道已成为绝大多数品牌的必然选择。 2022年&#xff0c;越来越多的品牌加入到自运营、自播的行列中&#xff0c;并且从…

golang gin——controller 模型绑定与参数校验

controller 模型绑定与参数校验 gin框架提供了多种方法可以将请求体的内容绑定到对应struct上&#xff0c;并且提供了一些预置的参数校验 绑定方法 根据数据源和类型的不同&#xff0c;gin提供了不同的绑定方法 Bind, shouldBind: 从form表单中去绑定对象BindJSON, shouldB…

【ONE·C++ || 异常】

总言 主要介绍异常。 文章目录 总言1、C异常1.1、C语言传统的处理错误的方式1.2、异常概念1.3、异常的基本用法1.3.1、异常的抛出和捕获1.3.1.1、异常的抛出和匹配原则1.3.1.2、 在函数调用链中异常栈展开匹配原则 1.3.2、异常的重新抛出1.3.2.1、演示一1.3.2.2、演示二 1.3.3…