多线程(线程互斥)

news2024/11/17 21:21:00

抢票代码编写

学习了前面有关线程库的操作后,我们就可以模拟抢票的过程
假设我们创建四个线程,分别代表我们的用户
然后设定总票数为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     else
 18     {                                                                                                                                                               
 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       else
 21       {
 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 TData
  9 {
 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       else
 32       {
 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); //对锁进行初始化,第二个参数为所得属性,一般设为nullptr
 47     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 enum
    9   {
   10      NEW = 0,
   11      RUNNING,
   12      EXITED
   13   }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   //返回线程的id
   30   //只有线程在运行的时候,才会有对应的线程id
   31   pthread_t GetTid()
   32   {
   33     if (_status == RUNNING)
   34     {
   35       return _tid;
   36     }                                                                                                                                                             
   37     else
   38     {
   39       return 0;
   40     }
   41   }
   42   //类成员函数具有默认参数this
   43   //但是会有新的问题
   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;    //线程id
   79    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 once
  2 
  3 #include <iostream>
  4 #include <pthread.h>
  5 
  6 class Mutex
  7 {
  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 LockGuard
 26 {
 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.coloradmin.cn/o/1077505.html

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

相关文章

【小记】Java将字符串中的EL表达式动态转换为Map中的值

项目中碰到了这样的问题&#xff0c;例如数据库中有一个字符串模版如下&#xff1a; ${userNamme}开启了一个新的${project}我们要替换里面的${}表达式&#xff0c;来实现动态的生成完整的信息~实现方式很简单如下&#xff1a; 引入pom依赖&#xff1a; <dependency>&l…

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

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

软件测试面试,5年测试工程师竟被面试官10分钟pass,这些问题一定要注意啊!

随着软件测试领域对于技术要求越来越清晰&#xff0c;到现在测试人员在市场上的岗位需求也变得越来越复杂。极大部分的企业都开始对自动化测试岗位有了更多的需要。自然而然&#xff0c;面试就相对于非常重要了。 网络上有着各式各样的测试框架的存在&#xff0c;我也不知道我…

Mall脚手架总结(三) —— MongoDB存储浏览数据

前言 通过Elasticsearch整合章节的学习&#xff0c;我们了解SpringData框架以及相应的衍生查询的方式操作数据读写的语法。MongoDB的相关操作也同样是借助Spring Data框架&#xff0c;因此这篇文章的内容比较简单&#xff0c;重点还是弄清楚MongoDB的使用场景以及如何通过Sprin…

LeetCode862 和至少为k的最短子数组

题目&#xff1a; 解析&#xff1a; 1、先构造前缀和数组 2、单调队列存放滑动窗口&#xff0c;目的求Sj-Si >k的情况下&#xff0c;窗口最小。 代码&#xff1a; class Solution {public int shortestSubarray(int[] nums, int k) {int n nums.length;long[] sums new …

网工内推 | base郑州,上市公司,最高15薪,五险一金全额缴

01 四方达 招聘岗位&#xff1a;网络工程师 职责描述&#xff1a; 1、负责公司数据中心&#xff08;机房&#xff09;的管理与运维工作。 2、负责公司服务器、路由器、防火墙、交换机等设备的管理、以及网络平台的运行监控和维护&#xff1b; 3、负责公司服务器运维管理工作、…

Git相关知识(1)

目录 1.初识Git 1.基础知识 2.centos中下载 2.基本操作 1.创建本地仓库 2.配置本地仓库 3.版本库、工作区、暂存区 4.添加文件 5.add和commit对git文件的作用 6.修改文件 7.版本回退 8.撤销修改 9.删除文件 3.分支操作 1.HEAD与分支 2.创建分支 3.删除分支 …

python中TagMe包的token获取

草&#xff0c;找了40分钟&#xff01;帮助大家少浪费时间。 1.注册Tagme&#xff0c;注册地址https://services.d4science.org/home &#xff08;我用邮箱注册一直说验证码不对&#xff0c;最后用Google账号注册的&#xff09; 2.找sobigdata 3.进入sobigdata后&#xff0c;…

17基于matlab卡尔曼滤波的行人跟踪算法,并给出算法估计误差结果,判断算法的跟踪精确性,程序已调通,可直接运行,基于MATLAB平台,可直接拍下。

17基于matlab卡尔曼滤波的行人跟踪算法&#xff0c;并给出算法估计误差结果&#xff0c;判断算法的跟踪精确性&#xff0c;程序已调通&#xff0c;可直接运行&#xff0c;基于MATLAB平台&#xff0c;可直接拍下。 17matlab卡尔曼滤波行人跟踪 (xiaohongshu.com)

MySQL总结练习题

目录 1.准备数据表 2.表之间的关系 3.题目 3.1 取得每个部门最高薪水的人员名称 3.2 哪些人的薪水在部门的平均薪水之上 3.3 取得部门中&#xff08;所有人的&#xff09;平均的薪水等级 3.4 不准用组函数&#xff08;Max &#xff09;&#xff0c;取得最高薪水 3.5 取…

Java基础之反射机制

背景&#xff1a;Java程序中的所有对一项都有两种类型&#xff1a;编译时类型和运行时类型&#xff08;由于多态导致的&#xff09;&#xff0c;这可能会导致对象的编译时类型和运行时类型不一致。 反射&#xff08;Reflection&#xff09;是被是为动态语言的关键&#xff0c;反…

速通Redis基础(二):掌握Redis的哈希类型和命令

目录 Redis 哈希类型简介 Redis 哈希命令 HSET HGET HEXISTS HDEL HKEYS HVALS HGETALL HMGET HLEN HSETNX ​编辑 HINCRBY HINCRBYFLOAT Redis的哈希类型命令小结 Redis 是一种高性能的键值存储数据库&#xff0c;支持多种数据类型&#xff0c;其中之…

jdbc+数据连接池

1.1 JDBC概念 JDBC 就是使用Java连接并操作数据库的一套API 全称&#xff1a;( Java DataBase Connectivity ) Java 数据库连接 1.2 JDBC优势 可随时替换底层数据库&#xff0c;访问数据库的Java代码基本不变 以后编写操作数据库的代码只需要面向JDBC&#xff08;接口&…

如何在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;是一个开放源代码的版本控制系统…

尚品甄选2023全新SpringBoot+SpringCloud企业级微服务项目

最适合新手入门的SpringBootSpringCloud企业级微服务项目来啦&#xff01;如果你已经学习了Java基础、SSM框架、SpringBoot、SpringCloud&#xff0c;想找一个项目来实战练习&#xff1b;或者你刚刚入行&#xff0c;需要可以写到简历中的微服务架构项目&#xff01; 项目采用前…

大运新能源天津车展深度诠释品牌魅力 为都市人群打造理想车型

如今&#xff0c;新能源汽车行业发展潜力巨大&#xff0c;不断吸引无数车企入驻新能源汽车赛道&#xff0c;而赛道的持续紧缩也让一部分车企很难找到突破重围的机会。秉持几十年的造车经验&#xff0c;大运新能源凭借雄厚的品牌实力从一众车企中脱颖而出。从摩托车到重卡&#…

在两个有序数组中找整体第k小的数

一、题目 给定两个已经排序的数组&#xff08;假设按照升序排列&#xff09;&#xff0c;然后找出第K小的数。比如数组A {1&#xff0c; 8&#xff0c; 10&#xff0c; 20}&#xff0c; B {5&#xff0c; 9&#xff0c; 22&#xff0c; 110}&#xff0c; 第 3 小的数是 8.。…

基于 Mtcnn(人脸检测)+Hopenet(姿态检测)+拉普拉斯算子(模糊度检测) 的人脸检测服务

写在前面 工作原因&#xff0c;顺便整理博文内容为一个 人脸检测服务分享以打包 Docker 镜像&#xff0c;可以直接使用服务目前支持 http 方式该检测器主要适用低质量人脸图片处理理解不足小伙伴帮忙指正&#xff0c;多交流&#xff0c;相互学习 对每个人而言&#xff0c;真正的…

11.动名词

一.什么是动名词 动名词是动词的另一种非谓语动词形式。动名词跟宾语或状语构成动名词短语&#xff0c;动名词跟动词不定式一样&#xff0c;都属于非谓语动词&#xff0c;不能作谓语&#xff0c;动名词或动名词短语可以作主语&#xff0c;表语&#xff0c;宾语&#xff0c;介词…

fastjson2与fury的巅峰对决,谁会笑到最后?

写在前面 两个月前&#xff0c;我们写过一篇关于fury和protostuff的性能对比的文章&#xff1a;谁才是真正的协议之王&#xff1f;fastjson2 vs fury&#xff0c;那时&#xff0c;两个协议框架各有千秋&#xff0c;不分伯仲&#xff0c;今天&#xff0c;看到fury推出了全新的0…