多线程(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.mzph.cn/news/94202.shtml

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

相关文章

十天学完基础数据结构-第二天(数据结构简介)

什么是数据结构&#xff1f; 在计算机科学中&#xff0c;数据结构是一种组织和存储数据的方式。它定义了数据的布局&#xff0c;以及对这些数据执行的操作。你可以把数据结构看作是计算机内存中的特定组织方式&#xff0c;就像图书馆中书籍的排列一样。 数据结构可以是各种形…

C++算法 —— 动态规划(9)完全背包问题

文章目录 1、动规思路简介2、完全背包【模板】3、零钱兑换4、零钱兑换Ⅱ5、完全平方数 背包问题需要读者先明白动态规划是什么&#xff0c;理解动规的思路&#xff0c;并不能给刚接触动规的人学习。所以最好是看了之前的动规博客&#xff0c;以及01背包博客&#xff0c;才能看完…

软件测试基础学习

注意&#xff1a; 各位同学们&#xff0c;今年本人求职目前遇到的情况大体是这样了&#xff0c;开发太卷&#xff0c;学历高的话优势非常的大&#xff0c;公司会根据实际情况考虑是否值得培养&#xff08;哪怕技术差一点&#xff09;&#xff1b;学历稍微低一些但是技术熟练的…

Linux:minishell

目录 1.实现逻辑 2.代码及效果展示 1.打印字符串提示用户输入指令 2.父进程拆解指令 3.子进程执行指令,父进程等待结果 4.效果 3.实现过程中遇到的问题 1.打印字符串的时候不显示 2.多换了一行 3.cd路径无效 4.优化 1.ll指令 2.给文件或目录加上颜色 代码链接 模…

Scala第十三章节

Scala第十三章节 1. 高阶函数介绍 2. 作为值的函数 3. 匿名函数 4. 柯里化 5. 闭包 6. 控制抽象 7. 案例: 计算器 scala总目录 文档资料下载

mstsc无法保存RDP凭据, 100%生效

问题 即使如下两项都打勾&#xff0c;其还是无法保存凭据&#xff0c;特别是连接Ubuntu (freerdp server)&#xff1a; 解决方法 网上多种复杂方法&#xff0c;不生效&#xff0c;其思路是修改后台配置&#xff0c;以使mstsc跟平常一样自动记住凭据。最后&#xff0c;如下的…

CentOS7安装Oracle XE记录

本文仅是CentOS7安装Oracle XE记录&#xff0c;供参考 1、下载安装包 oracle-xe-11.2.0-1.0.x86_64.rpm.zip 2、安装 &#xff08;1&#xff09;第一次安装 [rootnode1 opt]# cd Disk1/ [rootnode1 Disk1]# ll 总用量 309884 -rw-r--r-- 1 root root 317320273 9月 28 09…

ARM---实现1-100求和任务

.text .globl _start_start:mov r0, #0x1mov r1, #0x1 给r1加一固定1不变mov r2, #0x64 100判断bl sumcmp r1, r2 sum:addcc r1, r1,#0x1 r1自增addcc r0, r0, r1 r0求和movcc pc,lrstop:b stop.end

day49数据库 索引 事务

一、索引 什么是索引&#xff1a;索引是数据库库中用来提高查询效率的技术&#xff0c;类似于目录 为什么要使用索引&#xff1a;如果不使用索引&#xff0c;数据会零散的保存在磁盘块中&#xff0c;查询数据需要遍历每一个磁盘块&#xff0c;直到找到数据为止&#xff0c;效率…

linux python 保存图形savefig import matplotlib.pyplot as plt

import matplotlib.pyplot as plt # 绘制图形 mod.plot_history(20)# 保存图形 plt.savefig("my_training_ephoes_plot.png") # 保存为PNG格式 # 保存图形并设置dpi参数 plt.savefig("my_plot.png", dpi600) # 保存为PNG格式&#xff0c;设置dpi为300

力扣 -- 115. 不同的子序列

解题步骤&#xff1a; 参考代码&#xff1a; class Solution { public:int numDistinct(string s, string t) {int ns.size();int mt.size();//多开一行&#xff0c;多开一列vector<vector<double>> dp(m1,vector<double>(n1));for(size_t j0;j<n;j){dp[…

C#餐饮收银系统

一、引言 餐饮收银系统是一种用于管理餐馆、咖啡厅、快餐店等餐饮业务的计算机化工具。它旨在简化点餐、结账、库存管理等任务&#xff0c;提高运营效率&#xff0c;增强客户体验&#xff0c;同时提供准确的财务记录。C# 餐饮收银系统是一种使用C#编程语言开发的餐饮业务管理软…

pytorch_神经网络构建1

文章目录 pytorch简介神经网络基础分类问题分析:逻辑回归模型逻辑回归实现多层神经网络多层网络搭建保存模型 pytorch简介 为什么神经网络要自定义数据类型torch.tensor? tensor可以放在gpu上训练,支持自动求导,方便快速训练,同时支持numpy的运算,是加强版,numpy不支持这些 为…

【网络通信三要素】TCP与UDP快速入门

网络通信三要素 1.什么是网络编程&#xff1f; 可以让设备中的程序&#xff0c;与网络上其他设备中的程序进行数据交互&#xff0c;从而实现网络通信的手段&#xff0c;java.net.*包下提供了网络编程的解决方案 2.基本的通信架构 基本的通信架构有2种形式&#xff1a;CS架构…

项目进展(五)-修复PCB电路板,学习32位ADC芯片ADS1285

一、前言 上个月29号放假了&#xff0c;和朋友一起去了南京(人是真滴多)&#xff0c;师兄晚放假几天&#xff0c;结果在测试时不小心把12V和GND碰触到一起了&#xff0c;导致12V短路&#xff0c;电路板几乎瘫痪了。 今天下午到学校之后就开始着手寻找问题和修复&#xff0c;最…

手机自动直播系统源码交付与代理加盟注意事项解析!

随着直播行业的不断发展&#xff0c;手机自动直播已经成为了人们生活中不可或缺的一部分。手机无人直播软件成了香饽饽&#xff0c;各类手机实景直播APP大批量涌现。因为创业和技术门槛低&#xff0c;市场需求高&#xff0c;所以成了最火热创业赛道。那么如果是不懂技术的人群&…

Matlab随机数的产生

目录 1、常见分布随机数的产生 1.1 二项分布 1.2 泊松分布 1.3 几何分布 1.4 均匀分布&#xff08;离散&#xff0c;等可能分布&#xff09; 1.5 均匀分布&#xff08;连续型等可能&#xff09; 1.6 指数分布&#xff08;描述“寿命”问题&#xff09; 1.7 正态分布 1.8…

SpringBoot结合Redisson实现分布式锁

&#x1f9d1;‍&#x1f4bb;作者名称&#xff1a;DaenCode &#x1f3a4;作者简介&#xff1a;啥技术都喜欢捣鼓捣鼓&#xff0c;喜欢分享技术、经验、生活。 &#x1f60e;人生感悟&#xff1a;尝尽人生百味&#xff0c;方知世间冷暖。 &#x1f4d6;所属专栏&#xff1a;Sp…

C#,数值计算——Ranq2的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { /// <summary> /// Backup generator if Ranq1 has too short a period and Ran is too slow.The /// period is 8.5E37. Calling conventions same as Ran, above. /// </summary> …

基于Matlab求解高教社杯全国大学生数学建模竞赛(CUMCM2004A题)-奥运会临时超市网点设计(附上源码+数据)

文章目录 题目思路源码数据下载 题目 2008年北京奥运会的建设工作已经进入全面设计和实施阶段。奥运会期间&#xff0c;在比赛主场馆的周边地区需要建设由小型商亭构建的临时商业网点&#xff0c;称为迷你超市&#xff08;Mini Supermarket, 以下记做MS&#xff09;网&#xf…