多线程(pthread库)

POSIX线程库

引言

前面我们提到了Linux中并无真正意义上的线程
从OS角度来看,这意味着它并不会提供直接创建线程的系统调用,它最多给我们提供创建轻量级进程LWP的接口
但是从用户的角度来看,用户只认识线程啊!
因此,操作系统OS与用户两者之间,必定存在一个桥梁——库
这个线程库,对下能够将Linux提供的LWP进程接口进行封装,对上能够给用户进行线程控制的接口
这个库,我们就称作pthread库,在里面的绝大多数函数的名字都是以“pthread_”打头
在任何linux系统下,不管版本的老旧,都会默认自带,是一个原生线程库,等下我们也会进行验证

前提

但是,pthread线程库,并非随意就能使用,还需要我们编写代码时,进行一些附加操作

1.要使用这些函数库,要引入头文件<pthread.h>
2.链接这些线程函数库时要使用编译器命令的“-lpthread”选项

  1 mythreadTest:threadTest.cc2   g++ -o $@ $^ -std=c++11 -lpthread                                                                                                                                 3 .PHONY:clean4 clean:5   rm -f mythreadTest

线程创建

就像每个文件都有着其对应的inode编号,每个线程也有着自己的编号,它的类型时pthread_t类型
假如在编译器中一直跳转,寻找它最开始的定义
在这里插入图片描述
可以发现,它实际上就是一个unsigned long类型
不过具体这个编号有什么用,我们先按下不表
我们先介绍创建线程提供的pthread_create函数
man手册查该函数,会给出相应的函数介绍
可以看到该函数位于3号手册中,所以也符合我们前面的讲解,即该函数不是系统调用的函数,而是封装了linux系统的轻量级进程接口的函数
功能是创建一个新线程(create a new thread)
在这里插入图片描述
总共有4个参数

第一个参数thread
是线程id的地址(返回线程ID)
第二个参数attr,
设置线程的属性,attr为NULL表示使用默认属性,通常使用的时候都给nullptr,使用默认属性
第三个参数start_routine
是个函数地址,线程启动后要执行的函数,参数是void*,返回参数也是void*,是一个函数指针
第四个参数arg
是等下传给start_routine的参数(传给线程启动函数的参数)

有了上面的基础后,我们就可以先简单创建一个我们的线程
主线程输出对应的线程id
另外一个线程输出自己正在允许

    1 #include <iostream>2 #include <unistd.h>3 4 using namespace std;5 void* thread_run(void* args)6 {7   while(true)8   {9     cout << "new thread is running" << endl;10     sleep(1);                                                                                                                                                     11   }12 }13 14 int main()15 {16   pthread_t t;17   pthread_create(&t,nullptr,thread_run,nullptr);18 19   while(true)20   {21     cout << "main thread is running,thread id : " << t << endl;22     sleep(1);23   }24   return 0;25 }

可以看到,结果符合我们的预期
在这里插入图片描述
往命令行窗口输入lld + 对应文件名,即可看到该文件链接了什么库
可以看到,其中有一个pthread库,它对应的路径是/lib64/libpthread,也就和我们之前所说的任何Linux系统默认自带相应的pthread库说法,完美符合
在这里插入图片描述
但是我们现在只是创建了一个线程而已,所以,我们的代码肯定还是要改进,创建更多的线程的
我们采取数组的方式,我们知道数组名实际上就是首元素的地址,加上对应的i,实际对应的刚好就是数组里面每个元素的地址,而不用再取地址&
并且,我们可以开始研究第四个参数arg,它是主线程往新线程里面传的参数
那实际上能不能传过去呢?我们对传进来的参数args进行强制类型转换,然后打印相应的内容,假如能够打印相应的内容,则说明args这个参数的确能够是主线程往新线程里面传的参数

  1 #include <iostream>2 #include <unistd.h>3 #define NUM 104 using namespace std;5 void* thread_run(void* args)6 { 7   char* name = (char*)args;8 9   while(true)10   {11     cout << "new thread: " << name << " is running" << endl;12     sleep(1);13   }14 15   return nullptr;                                                                                                                                                   16 }17 18 int main()19 {20   pthread_t tid[NUM];21   for (int i = 0;i < NUM;i++)22   {23      char tname[64];24      snprintf(tname,sizeof(tname),"thread-%d",i + 1);25      pthread_create(tid + i,nullptr,thread_run,tname);26   }27 28 29   while(true)30   {31     cout << "main thread is running" << endl;32     sleep(1);33   }34   return 0;35 }

但是,打印出来的结果,却不符合我们的预期
第一,我们预想的是,每个线程的编号都应该不同,即每个线程的名字都不一样,毕竟我们循环往tname这个数组里面放内容的时候,用的是不同的i
第二,有部分线程输出代码紧挨在一起,并且主线程并不是最先运行的,反而是新线程先运行
在这里插入图片描述
对于第二个问题,我们其实可以解释,在进程的一章中我们就已经提到过,哪个进程先被调度,其实是不确定的,同样的,线程也是我们调用轻量级进程接口创建出来的,肯定也是符合这个规律,所以谁先被调度,完全取决于调度器决定,先创建的线程,不一定被调度
对于第一个问题,就有点难理解
实际上是由于线程共享的是同一份资源,即便这只是一个临时变量
因此,tname里面存的地址,在不同线程看来都是相同的
所以往里面同时写数据,就会覆盖原有tname空间的旧内容
最后剩下的,仅仅是最后调度的线程的名字
那我们要怎么修改呢?
一种简单的方式,就是直接new相应的空间
(不过要注意,此时使用snprintf函数的时候,就不能再直接sizeof,这样计算的就单纯只会是指针的大小,所以这里直接指定64字节,毕竟整个空间也就64字节)
对于每个线程来说,都会new出新的自己的空间,这样放的数据就不会再被覆盖
相当于每个线程,都有了自己的房子,从此井水不犯河水

  1 #include <iostream>2 #include <unistd.h>3 #define NUM 104 using namespace std;5 void* thread_run(void* args)6 {7   char* name = (char*)args;8 9   while(true)10   {11     cout << "new thread: " << name << " is running" << endl;12     sleep(1);13   }14   delete name;                                                                                                                                                      15   return nullptr;16 }17 18 int main()19 {20   pthread_t tid[NUM];21   for (int i = 0;i < NUM;i++)22   {23      char* tp = new char[64];24      snprintf(tp,64,"thread-%d",i + 1);25      pthread_create(tid + i,nullptr,thread_run,tp);26   }27 28 29   while(true)30   {31     cout << "main thread is running" << endl;32     sleep(1);33   }34   return 0;35 }

经过修改后的运行效果,就符合我们的预期了
在这里插入图片描述

线程终止

前面我们提到了在linux系统下,是没有对应具体线程的实现!而是采用复用的方式
所以,进程有的特性,线程往往也会有

主线程提前退出

假如主线程现在提前退出了,说不再和其它新线程一起玩,会发生什么情况呢?
将上面主线程的代码修改一下,把循环去掉
在这里插入图片描述
此时,再编译运行我们的代码,会得到下面的结果
可以看到,一旦主线程退出了,其它的所有新线程,就会全部强制退出
在这里插入图片描述
为什么会出现这种情况呢?
就是因为线程是进程的一个执行分支,线程异常了,发送信号是给进程发信号,进程挂了,所有依附于它的线程,全部都走不了,覆巢之下,安有完卵,指的就是这个道理
同样的,假如其中一个线程调用了exit函数,那请问最后的结果会是怎么样呢?
我们同样可以修改相应的代码,在循环中加入相应的exit函数
在这里插入图片描述
可以看到,只有几个线程成功输出了自己的编号,程序就自动停止了
所以实际的情况就是,有几个线程成功被创建,但是其中有一个线程执行exit函数,然后全部线程都挂掉了
在这里插入图片描述
只要有任何一个线程调用exit函数,整个进程中,所有的线程都会全部退出
关键不是主线程还是新线程的问题,而是大家都是一体的,同生共死

阻塞等待

正是由于线程和进程是有很多相似之处的,进程有父进程阻塞等待,回收子进程的操作
线程也会有相应的概念
主线程需要等待子线程,然后进行相应的回收,否则子线程就会陷入僵尸状态
在pthread库里面就已经提供了相应主线程等待的库函数pthread_join
调用该函数,主线程就会阻塞,并回收相应退出的新线程(join with a terminated thread)
在这里插入图片描述
它总共有两个参数‘
第一个参数thread,就是我们之前提到过的线程id
第二个参数retval,是一个二级指针void**,为什么是二级指针呢?
因为它是一个输出型参数,早在C语言函数中我们就已经学过,由于C语言中没有引用的概念,因此,在函数内部进行赋值,其实改变的都是形参,并不会改变实参
想要改变实参,就需要传相应的指针
想传int出来,就要int*
想传int出来,就要int**作为参数
同理,假如我们返回的参数此时是一个void
类型的,那用void**接收,也就非常合理了
在这里插入图片描述
它的返回值和前面提到过的pthread_create函数相同
成功的话,就返回0;否则,返回一个错误的数字

  1 #include <iostream>2 #include <unistd.h>3 #define NUM 104 using namespace std;5 void* thread_run(void* args)6 { 7   char* name = (char*)args;8 9   while(true)10   {11     cout << "new thread: " << name << " is running" << endl;12     sleep(1);13   }                                                                                                                                                             14   delete name;                                                                                                                                                  15   return nullptr;                                                                                                                                               16 }                                                                                                                                                               17                                                                                                                                                                 18 int main()                                                                                                                                                      19 {                                                                                                                                                               20   pthread_t tid[NUM];                                                                                                                                           21   for (int i = 0;i < NUM;i++)                                                                                                                                   22   {                                                                                                                                                             23      char* tp = new char[64];                                                                                                                                   24      snprintf(tp,64,"thread-%d",i + 1);                                                                                                                         25      pthread_create(tid + i,nullptr,thread_run,tp);                                                                                                             26   }                                                                                                                                                                 27 28   for (int i = 0;i < NUM;i++)29   {30     pthread_join(tid[i],nullptr);31   }32 33   return 0;34 }

终止方式

既然,我们知道主线程,需要阻塞等待子线程退出,并回收相应的子线程
那了解子线程有多少种退出方式,就非常有必要
子线程总共有三种退出方式
第一种方式,线程函数执行完毕,此时直接返回nullptr,线程就会退出

  1 #include <iostream>2 #include <unistd.h>3 #define NUM 104 using namespace std;5 void* thread_run(void* args)6 {                                                                                                                                                                   7   char* name = (char*)args;8 9   while(true)10   {11     cout << "new thread: " << name << " is running" << endl;12     sleep(1);13     break;14   }15   delete name;16   return nullptr;17 }18 int main()19 {                                                                                                                                    20   pthread_t tid[NUM];                                                                                                                21   for (int i = 0;i < NUM;i++)                                                                                                        22   {                                                                                                                                  23      char* tp = new char[64];                                                                                                        24      snprintf(tp,64,"thread-%d",i + 1);                                                                                              25      pthread_create(tid + i,nullptr,thread_run,tp);                                                                                  26   }                                                                                                                                  27                                                                                                                                      28   for (int i = 0;i < NUM;i++)                                                                                                        29   {                                                                                                                                  30     int n = pthread_join(tid[i],nullptr);                                                                                            31     //errno变量只有一个,而线程有多个,作同时修改,可能会互相影响                                                                    32     if(n!= 0)   cerr << "pthread_join error" << endl;                                                                                33   }                                                                                                                                  34                                                                                                                                      35   return 0; 

第二种方式,pthread库里面提供了相应的线程退出函数pthread_exit
在这里插入图片描述
它的参数retval,为一个输出型参数
没错,和我们之前提到的pthread_join的参数名字是相同的,也就意味着两者肯定有所关联
通过返回retval,我们对应的主线程就可以接收到对应的错误信息
那为什么我们不通过设置全局变量errno来输出对应的错误信息呢?
因为不同线程对于这个全局变量是共享的,全部线程都同时使用一个全局变量,就可能会出现覆盖等等问题,导致出错了也可能不知道
因此,pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)
而是将错误码通过返回值返回
还有一个好处是,对于pthreads函数的错误,通过返回值判定,要比读取线程内的errno变量的开销更小

  1 #include <iostream>2 #include <unistd.h>3 #define NUM 104 using namespace std;5 void* thread_run(void* args)6 { 7   char* name = (char*)args;8 9   while(true)10   {11     cout << "new thread: " << name << " is running" << endl;12     break;13   }14   delete name;15   pthread_exit(nullptr);                                                                                                                                            16 }17 18 19 int main()20 {21   pthread_t tid[NUM];22   for (int i = 0;i < NUM;i++)23   {24      char* tp = new char[64];25      snprintf(tp,64,"thread-%d",i + 1);26      pthread_create(tid + i,nullptr,thread_run,tp);27   }28 29   for (int i = 0;i < NUM;i++)30   {31     int n = pthread_join(tid[i],nullptr);32     //errno变量只有一个,而线程有多个,作同时修改,可能会互相影响33     if(n!= 0)   cerr << "pthread_join error" << endl;34   }35   cout << "all thread quit" << endl;36   return 0;37 }
  1 #include <iostream>2 #include <unistd.h>3 #include <pthread.h>4 #include <string>5 #include <ctime>6 #define NUM 107 using namespace std;8 9 10 class ThreadData11 {12 public:13   ThreadData(const string& name,int id,time_t createTime):_name(name),_id(id),_createTime((uint64_t)createTime)14   {}15   ~ThreadData()16   {}17 public:18     string _name;19     int _id;20     uint64_t _createTime;21 };22 void* thread_run(void* args)23 {24   ThreadData* tp = static_cast<ThreadData*>(args);25 26   while(true)27   {28     cout << "thread is running,name: " << tp->_name << " create time: "<< tp->_createTime << " index:" << tp->_id << endl;                                          29     break;30   }31   delete tp;32   pthread_exit((void*)2);33 }34 35 36 int main()37 {38   pthread_t tid[NUM];39   for (int i = 0;i < NUM;i++)40   {  41      char tname[64];42      snprintf(tname,sizeof(tname),"thread-%d",i + 1);43      ThreadData* tp = new ThreadData(tname,i + 1,time(nullptr));44      pthread_create(tid + i,nullptr,thread_run,tp);45   }46 47   void* ret = nullptr;48   for (int i = 0;i < NUM;i++)49   {50     int n = pthread_join(tid[i],&ret);51     //errno变量只有一个,而线程有多个,作同时修改,可能会互相影响52     if(n!= 0)   cerr << "pthread_join error" << endl;53 54     cout << "thread quit: " << (uint64_t)ret << endl;55   }56   cout << "all thread quit" << endl;57   return 0;58 }

最后一种方式,是一个线程可以调用pthread_ cancel函数终止同一进程中的另一个线程
在这里插入图片描述
整个函数只需要一个参数,也即是我们的线程id
它的功能就是取消一个执行中的线程
成功返回0;反之,则返回错误码

类型转换

在C,或者C++中,我们都知道,一个类型的值赋值给不匹配的类型变量,就会发生报错
但是,我们仔细思考一下,在计算机的眼里,不同数据有区别吗?
都只是0,1的集合罢了
所以,报错是编译器检测发现你类型不匹配,然后报错,显示无法编译你的代码
所谓的类型转换,就是让我们骗过编译器,让数据能够赋到我们想要的变量中
比如说下面的代码,1还是那个1,但是是int类型
你需要将它类型转换,告诉编译器,这个1其实是一个地址,这样才能成功赋值

void* ret = (void*)1;

进一步思考的话,类型转换也告诉了OS,这究竟是什么类型变量
这非常关键,决定我们将它存到哪里,它的偏移地址是什么等等,这样我们以后才能成功访问到这个数据

void*

所以,为什么无论是我们pthread_create函数,还是我们的pthread_exit函数,它们的参数中,设计的都是void*
为的是什么?
为的就是我们让我们传入参数和返回参数的可塑性更强,它并非局限我们只能传一个字符串作为线程函数传入参数,或者只能返回对应的错误码
我们是可以传int,double*等等所有的指针,甚至我们是可以传对象指针进去!!!*
只需要void*接收,然后再类型转换为我们想要的类型,就可以让OS找到对应的资源!!!
下面这段代码,就实现了传一个对象进去线程函数里面,并且通过返回这个对象的指针,将里面处理好的结果带出来

整段代码实现的功能
就是让不同的线程,分别实现从1到对应top数字的求和
原本的串行执行,转变为现在的并发执行

  1 #include <iostream>2 #include <unistd.h>3 #include <pthread.h>4 #include <string>5 #include <ctime>6 #define NUM 107 using namespace std;8 enum{ ERROR = 0,OK };9 10 class ThreadData11 {12 public:13   ThreadData(const string& name,int id,time_t createTime,int top):_name(name),_id(id),_createTime((uint64_t)createTime),_status(OK),_top(top),_result(0)14   {}15   ~ThreadData()16   {}17 public:18     //传入的参数19     string _name;20     int _id;21     uint64_t _createTime;22     23     //返回的参数24     int _status; //该线程的参数25     int _top;26     int _result; //结果是什么27     //char arr[n];28 };29 void* thread_run(void* args)30 {                                                                                                                                                                   31   ThreadData* tp = static_cast<ThreadData*>(args);32 33   for (int i = 1;i <= tp->_top;i++)34   {35       tp->_result += i;36   }37 38   cout << "tp->_name: " << tp->_name << endl;39   return tp;40 }41 42 int main()43 {44   pthread_t tid[NUM];45   for (int i = 0;i < NUM;i++)46   {  47      char tname[64];48      snprintf(tname,64,"thread-%d",i + 1);49      //多传入一个参数,用来在创建线程,执行相应任务所加到的对应的数字50      ThreadData* tp = new ThreadData(tname,i + 1,time(nullptr),100 + 4*i);51      pthread_create(tid + i,nullptr,thread_run,tp);52      sleep(1);53   }54 55   void* ret = nullptr;56   for (int i = 0;i < NUM;i++)57   {58     int n = pthread_join(tid[i],&ret);59     //errno变量只有一个,而线程有多个,作同时修改,可能会互相影响60     if(n!= 0)   cerr << "pthread_join error" << endl;61     ThreadData* tp = static_cast<ThreadData*> (ret);62     if (tp->_status == OK)63     {64        cout << "thread name: " << tp->_name << " 计算的结果为:" << tp->_result << "[0," << tp->_top << "]" << endl;65     }66     delete tp;67   }68   cout << "all thread quit" << endl;69   return 0;70 }

输出的结果如下图所示:
在这里插入图片描述

让线程获取自己的线程id

那线程有自己的编号,能不能让线程获取对应自己的编号呢?
pthread库中也提供了相应的接口pthread_self
在这里插入图片描述
函数参数是没有的,直接调用即可输出当前线程的id是什么
我们可以编写一段程序,来看看对应的线程id,同时返回到主线程,也打印出来对比一下

  1 #include <iostream>2 #include <unistd.h>3 #include <pthread.h>4 #include <string>5 #include <ctime>6 #define NUM 107 using namespace std;8 9 void* thread_create(void* args)10 {11    const char* name = static_cast<const char*>(args);12    int cnt = 5;13    while(cnt--)14    {15      cout << name << " is running..." << "  obtain my tid: " << pthread_self()<< endl;                                                                              16      sleep(1);17    }18 19    pthread_exit((void*)11);20 }21 int main()22 {23   pthread_t tid;24   pthread_create(&tid,nullptr,thread_create,(void*)"thread 1");25 26   void* ret = nullptr;27   int n = pthread_join(tid,&ret);28   if (n != 0)  cerr << "thread_join error: " << endl;29   cout << "new thread exit: " << (uint64_t)ret << endl;30   cout << " quit thread id: " << tid << endl;31   return 0;32 }

可以看到线程的id通过pthread_self函数,是能够成功获取的
在这里插入图片描述

分离线程

前面我们提到,主线程会阻塞等待新线程退出
但是阻塞等待,也就意味着在这期间,主线程并不能干任何事情
那假如我们想要主线程不阻塞等待,让新线程自己回收自己,又应该怎么操作呢?
pthread库中实际上,确实提供类似的接口函数pthread_detach
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
但假如我们不关心线程的返回值,join是一种负担,此时,我们就可以修改对应线程的属性,让线程自己退出时,自动释放资源

PS:假如一个线程分离detach后,此时就不能再join了,函数会发生报错
joinable和分离是冲突的,一个线程不能既是joinable又是分离的

下面,我们简单写一段代码来验证joinable和分离,两者是冲突的这个结论

  1 #include <iostream>2 #include <pthread.h>3 #include <unistd.h>4 #include <cstring>5 #include <string>6 using namespace std;7 void* threadRoutine(void* args)8 {9   string name = static_cast<const char*>(args);10   int cnt = 5;11   while(cnt)12   {13     cout << name << " : " << cnt-- << endl;14     sleep(1);15   }16   return nullptr;17 }18 int main()19 {20   pthread_t tid;21   pthread_create(&tid,nullptr,threadRoutine,(void*)"thread 1");22   pthread_detach(tid);                                                                                                                                              23   int n = pthread_join(tid,nullptr);24   if(0 != n)25   {26     cerr << "error: " << n << " : "<< strerror(n) << endl;27   }28   return 0;29 30 }

我们再创建一个新线程后,再detach掉对应的新线程
可以看到运行结果显示,程序会直接发生报错
在这里插入图片描述
但除了在主线程进行detach外,也可以在新线程中,让新线程自己detach
比如说下面的代码

  1 #include <iostream>2 #include <pthread.h>3 #include <unistd.h>4 #include <cstring>5 #include <string>6 using namespace std;7 void* threadRoutine(void* args)8 {9   pthread_detach(pthread_self());10   string name = static_cast<const char*>(args);11 12   int cnt = 5;13   while(cnt)14   {15     cout << name << " : " << cnt-- << endl;16     sleep(1);17   }18   return nullptr;19 }20 int main()21 {22   pthread_t tid;23   pthread_create(&tid,nullptr,threadRoutine,(void*)"thread 1");24   int n = pthread_join(tid,nullptr);                                                                                                                                25   if(0 != n)26   {27     cerr << "error: " << n << " : "<< strerror(n) << endl;28   }29   return 0;30 31 }

但是运行的结果却并不符合预期,我们并没有看到报错,可以发现新线程照样可以正常跑
在这里插入图片描述
这是为什么呢?
原因就在于我们刚开始说的,线程谁先执行并不确定,线程可能被创建出来,但是并没有运行
在上述的代码中就是如此,新线程虽然被创建了,但是并没有被允许
此时主线程检测新线程的属性,可以发现仍然是joinable的,然后允许相关的join代码,主线程被挂起,此时新线程才被执行
因此,假如我们要让新线程自己释放自己的资源的话,我们还需要先让主线程sleep上对应的秒数,让新线程先执行
在这里插入图片描述
此时允许的结果就符合我们之前的说法了
在这里插入图片描述
正是由于这个的缘故,因此我们一般分离线程,采取的方式都是建议主线程直接detach,而不是自detach

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

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

相关文章

wxWidgets(1):在Ubuntu 环境中搭建wxWidgets 库环境,安装库和CodeBlocks的IDE,可以运行demo界面了,继续学习中

1&#xff0c;选择使用 wxWidgets 框架 选择这个主要是因为完全的开源&#xff0c;不想折腾 Qt的库&#xff0c;而且打包的文件比较大。 网络上面有很多的对比&#xff0c;而且使用QT的人比较多。 但是我觉得wxwidgets 更加偏向 c 语法本身&#xff0c;也有助学习C。 没有太多…

【算法分析与设计】回溯法(上)

目录 一、学习要点1.1 回溯法1.2 问题的解空间1.3 0-1背包问题的解空间1.4 旅行售货员问题的解空间1.5 生成问题状态的基本方法 二、回溯法的基本思想三、回溯算法的适用条件四、递归回溯五、迭代回溯六、子集树与排列树七、装载问题八、批处理作业调度问题 一、学习要点 理解回…

Kotlin前置检测判断check,require,requireNotNull

Kotlin前置检测判断check&#xff0c;require&#xff0c;requireNotNull &#xff08;1&#xff09;check fun main(args: Array<String>) {val b falsecheck(b) {println("check $b")}println("end") } check监测到值非真时候&#xff0c;抛出一…

【数据结构与算法】通过双向链表和HashMap实现LRU缓存 详解

这个双向链表采用的是有伪头节点和伪尾节点的 与上一篇文章中单链表的实现不同&#xff0c;区别于在实例化这个链表时就初始化了的伪头节点和伪尾节点&#xff0c;并相互指向&#xff0c;在第一次添加节点时&#xff0c;不需要再考虑空指针指向问题了。 /*** 通过链表与HashMa…

Python 无废话-基础知识元组Tuple详讲

“元组 Tuple”是一个有序、不可变的序列集合&#xff0c;元组的元素可以包含任意类型的数据&#xff0c;如整数、浮点数、字符串等&#xff0c;用()表示&#xff0c;如下示例&#xff1a; 元组特征 1) 元组中的各个元素&#xff0c;可以具有不相同的数据类型&#xff0c;如 T…

Python-Flask:编写自动化连接demo脚本:v1.0.0

主函数&#xff1a; # _*_ Coding : UTF-8 _*_ # Time : 13:14 # Author : YYZ # File : Flask # Project : Python_Project_爬虫 import jsonfrom flask import Flask,request,jsonify import sshapi Flask(__name__)# methods: 指定请求方式 接口解析参数host host_info[…

05. 机器学习入门 - 动态规划

文章目录 从一个案例开始动态规划 Hi, 你好。我是茶桁。 咱们之前的课程就给大家讲了什么是人工智能&#xff0c;也说了每个人的定义都不太一样。关于人工智能的不同观点和方法&#xff0c;其实是一个很复杂的领域&#xff0c;我们无法用一个或者两个概念确定什么是人工智能&a…

在visual studio里配置Qt插件并运行Qt工程

Qt插件&#xff0c;也叫qt-vsaddin&#xff0c;它以*.vsix后缀名结尾。从visual studio 2010版本开始&#xff0c;VS支持Qt框架的开发&#xff0c;Qt以插件方式集成到VS里。这里在visual studio 2019里配置Qt 5.14.2插件&#xff0c;并配置Qt环境。 1 下载VS2019 下载VS2019,官…

跟着顶级科研报告IPCC学绘图:温度折线/柱图/条带/双y轴

复现IPCC气候变化过程图 引言 升温条带Warming stripes&#xff08;有时称为气候条带&#xff0c;目前尚无合适且统一的中文释义&#xff09;是数据可视化图形&#xff0c;使用一系列按时间顺序排列的彩色条纹来视觉化描绘长期温度趋势。 在IPCC报告中经常使用这一方案 IPCC是…

嵌入式Linux应用开发-基础知识-第十九章驱动程序基石④

嵌入式Linux应用开发-基础知识-第十九章驱动程序基石④ 第十九章 驱动程序基石④19.7 工作队列19.7.1 内核函数19.7.1.1 定义 work19.7.1.2 使用 work&#xff1a;schedule_work19.7.1.3 其他函数 19.7.2 编程、上机19.7.3 内部机制19.7.3.1 Linux 2.x的工作队列创建过程19.7.3…

BASH shell脚本篇2——条件命令

这篇文章介绍下BASH shell中的条件相关的命令&#xff0c;包括&#xff1a;if, case, while, until, for, break, continue。之前有介绍过shell的其它基本命令&#xff0c;请参考&#xff1a;BASH shell脚本篇1——基本命令 1. If语句 if语句用于在顺序执行语句的流程中执行条…

八大排序(三)堆排序,计数排序,归并排序

一、堆排序 什么是堆排序&#xff1a;堆排序&#xff08;Heap Sort&#xff09;就是对直接选择排序的一种改进。此话怎讲呢&#xff1f;直接选择排序在待排序的n个数中进行n-1次比较选出最大或者最小的&#xff0c;但是在选出最大或者最小的数后&#xff0c;并没有对原来的序列…

Python无废话-办公自动化Excel修改数据

如何修改Excel 符合条件的数据&#xff1f;用Python 几行代码搞定。 需求&#xff1a;将销售明细表的产品名称为PG手机、HW手机、HW电脑的零售价格分别修改为4500、5500、7500&#xff0c;并保存Excel文件。如下图 Python 修改Excel 数据&#xff0c;常见步骤&#xff1a; 1&…

docker 基本操作

目录 一、docker 概述 二、容器 2.1容器的特性 2.2namespace的六项隔离 三、docker与虚拟机的区别 四、Docker核心概念 五、docker 基本操作命令 镜像操作 1、搜索镜像 2、获取镜像 3、查看镜像信息 ​编辑 4、查看下载的镜像文件信息 5、查看下载到本地的所有镜…

搭建智能桥梁,Amazon CodeWhisperer助您轻松编程

零&#xff1a;前言 随着时间的推移&#xff0c;人工智能技术以惊人的速度向前发展&#xff0c;正掀起着全新的编程范式革命。不仅仅局限于代码生成&#xff0c;智能编程助手等创新应用也进一步提升了开发效率和代码质量&#xff0c;极大地推动着软件开发领域的快速繁荣。 当前…

SpringCloud(一)Eureka、Nacos、Feign、Gateway

文章目录 概述微服务技术对比 Eureka服务远程调用服务提供者和消费者Eureka注册中心搭建注册中心服务注册服务发现Ribbon负载均衡负载均衡策略饥饿加载 NacosNacos与Eureka对比Nacos服务注册Nacos服务分集群存储NacosRule负载均衡服务实例权重设置环境隔离 Nacos配置管理配置热…

用于自然语言处理的 Python:理解文本数据

一、说明 Python是一种功能强大的编程语言&#xff0c;在自然语言处理&#xff08;NLP&#xff09;领域获得了极大的普及。凭借其丰富的库集&#xff0c;Python 为处理和分析文本数据提供了一个全面的生态系统。在本文中&#xff0c;我们将介绍 Python for NLP 的一些基础知识&…

2023 彩虹全新 SUP 模板,卡卡云模板修复版

2023 彩虹全新 SUP 模板&#xff0c;卡卡云模板&#xff0c;首页美化&#xff0c;登陆页美化&#xff0c;修复了 PC 端购物车页面显示不正常的问题。 使用教程 将这俩个数据库文件导入数据库&#xff1b; 其他的直接导入网站根目录覆盖就好&#xff1b; 若首页显示不正常&a…

计算机网络学习易错点(持续更新~~~)

目录 概述 1.internet和Internet的区别 2.面向连接和无连接 3.不同的T 4.传输速率和传播速率 5.传播时延和传输时延&#xff08;发送时延&#xff09; 6.语法&#xff0c;语义和同步 一.物理层 1.传输媒体与物理层 2.同步通信和异步通信 3.位同步&#xff08;比特同…

nginx多文件组织

背景&#xff1a; nginx的话&#xff0c;有时候&#xff0c;想部署多个配置&#xff0c;比如&#xff1a;使用不同的端口配置不同的web工程。 比如&#xff1a;8081部署&#xff1a;项目1的web页面。 8082部署&#xff1a;项目2的web页面。 1)nginx.conf worker_processes…