【Linux系统篇】:Linux线程控制基础---线程的创建,等待与终止

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:Linux篇–CSDN博客

在这里插入图片描述

文章目录

  • 一.线程创建
  • 二.线程等待
  • 三.线程终止
  • 四.扩展内容
    • 1.重谈`pthread_create`函数
    • 2.C++11线程库
    • 3.线程栈结构
    • 4.线程局部存储
    • 5.分离线程

POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以pthread_开头的;
  • 要使用这些线程的相关函数,需通过引入头文件<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的-lpthread选项。

一.线程创建

线程创建函数

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

功能:创建一个新的线程

参数

  • thread:返回线程的ID;
  • attr:设置线程的属性,为空时(nullptr)表示使用默认属性;
  • start_routine:函数地址,线程启动后要执行的函数
  • arg:传入线程启动函数的参数

返回值:成功返回0;失败返回错误码

测试代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;// 新线程的执行函数
void *pthreadRoution(void *args){while(true){cout << "new thread, pid: " << getpid() << endl;sleep(1);}
}int main(){pthread_t tid;// 主线程创建一个新线程pthread_create(&tid, nullptr, pthreadRoution, nullptr);// 主线程while(true){cout << "main thread, pid: " << getpid() << endl;sleep(1);}return 0;
}

在这里插入图片描述

结合创建的线程重新认识一下线程的相关概念:

1.任何一个线程被干掉,其余线程包括整个进程都会被干掉,所以这就是为什么线程的健壮性很差

在这里插入图片描述

2.在多线程情况下,一个方法可以被多个执行流同时执行,这种情况就是show函数被重入了

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;// show函数方法
void show(const string &name){cout << name << "say# " << "hello thread" << endl;
}// 新线程的执行函数
void *pthreadRoution(void *args){while(true){//cout << "new thread, pid: " << getpid() << endl;show("[new thread]");sleep(2);}
}int main(){pthread_t tid;// 主线程创建一个新线程pthread_create(&tid, nullptr, pthreadRoution, nullptr);// 主线程while(true){//cout << "main thread, pid: " << getpid() << endl;show("[main thread]");sleep(2);}return 0;
}

在这里插入图片描述

3.未初始化和已初始化的全局变量在所有线程中是共享的

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;int g_val = 100;
// 新线程的执行函数
void *pthreadRoution(void *args){while(true){printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);sleep(2);}
}int main(){pthread_t tid;// 主线程创建一个新线程pthread_create(&tid, nullptr, pthreadRoution, nullptr);// 主线程while(true){printf("main thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);sleep(2);g_val++;}return 0;
}

在这里插入图片描述

根据上面的例子可以发现,线程之前想要通信会变得非常简单,因为线程之间天然的就具有共享资源

二.线程等待

一般而言,主线程一定会是最后退出的,因为其他线程是由主线程创建的,主线程就要对创建出来的新线程做管理,和父进程等待回收子进程一样,主线程也要等待其他线程进行回收,否则就会造成类似于僵尸进程的问题,比如内存泄漏;同理,主线程创建新线程肯定是要执行一些任务,最后新线程的执行情况也是要返回给主线程的。

所以线程等待和进程等待同理,两个目的:

1.防止新线程造成内存泄露(主要目的)

2.如果需要,主线程也可以获取新线程的执行结果

线程等待函数

#include <pthread.h>int pthread_join(pthread_t thread, void **retval);

参数

  • thread:线程ID
  • value_prt:二级指针,指向一个指针的地址,输出型参数,用来获取线程的返回值;如果不关心返回值,可以直接设置为空指针

返回值:成功返回0;失败返回错误码。

调用该函数的线程将挂起等待,直到ID为thread的线程终止;一般都是主线程调用,所以主线程等待时默认是阻塞式等待

测试代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;int g_val = 100;
// 新线程的执行函数
void *pthreadRoution(void *args){int cnt = 5;while (true){printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);sleep(2);cnt--;if (cnt == 0){break;}}return (void *)100;
}int main(){pthread_t tid;// 主线程创建一个新线程pthread_create(&tid, nullptr, pthreadRoution, nullptr);void *retval;pthread_join(tid, &retval);cout << "main thread quit ..., ret: " << (long long int)retval << endl;return 0;
}

在这里插入图片描述

为什么线程等待时不用考虑异常呢?

因为根本做不到,一旦其中一个线程出现异常,整个进程也就直接终止退出了。异常问题是由进程考虑的,线程只需要考虑正常情况即可。

三.线程终止

线程终止时直接使用return语句返回是其中一种方法,除了这个还用其他方法,

先测试使用exit终止线程:

//线程等待的测试代码中使用exit终止退出
void *pthreadRoution(void *args){int cnt = 5;while (true){printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);sleep(1);cnt--;if (cnt == 0){break;}}exit(11);//return (void *)100;
}

在这里插入图片描述

最后的结果现象就是,新线程终止退出后,主线程并没有回收新线程,这是因为调用exit函数使整个进程都终止退出了。

任何一个线程调用exit,都表示整个进程终止;exit是用来终止进程的,不能用来终止线程。

线程终止可以使用线程库中的pthread_exit函数

线程终止函数

void pthread_exit(void *value_ptr);

参数value_ptr:指向线程终止时返回值的地址;注意,要返回的指针不能指向一个局部变量

返回值:无返回值,线程结束的时候无法返回到他的调用者。

测试代码:

void *pthreadRoution(void *args){int cnt = 5;while (true){printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);sleep(1);cnt--;if (cnt == 0){break;}}pthread_exit((void *)100);// exit(11);// return (void *)100;
}

在这里插入图片描述

如果主线程先退出,创建出的新线程后退出,最后的现象就是一旦主线程,其余的线程都会退出,也就是整个进程退出

int main(){pthread_t tid;// 主线程创建一个新线程pthread_create(&tid, nullptr, pthreadRoution, nullptr);// 主线程一秒后退出sleep(1);return 0;void *retval;pthread_join(tid, &retval);cout << "main thread quit ..., ret: " << (long long int)retval << endl;return 0;
}

在这里插入图片描述

因为主线程是在main函数中,在main函数return 相当于调用exit函数终止整个进程;

需要注意的是,使用pthread_exit或者return这两种方式来终止线程时,返回的指针所指向的内存单元必须是全局的或者是在堆区上分配的,不能在线程当前执行的函数的栈上分配,因为一旦线程结束函数调用时,栈上分配的空间就会自动释放

一个线程终止退出,除了上面的的returnpthread_exit函数两种方式以外,还有一种退出方式:线程取消,调用pthread_cancel函数

int pthread_cancel(pthread_t thread);

参数thread:线程ID

返回值:成功返回0;失败返回错误码

线程取消是由主线程调用pthread_cancel函数像目标线程发送一个终止请求,测试代码:

int main(){pthread_t tid;// 主线程创建一个新线程pthread_create(&tid, nullptr, pthreadRoution, nullptr);sleep(1);pthread_cancel(tid);void *retval;pthread_join(tid, &retval);cout << "main thread quit ..., ret: " << (long long int)retval << endl;return 0;
}

在这里插入图片描述

线程取消后,退出结果就会设置成一个宏PTHREAD_CANCELED(表示-1),线程等待就会获取到退出结果-1。

总结:如果需要只终止某个线程而不终止整个进程,有三种方式

1.线程执行的函数return;主线程不适用。

2.线程调用pthread_exit终止自己。

3.一个线程可以调用pthread_cancel终止同一进程中的另一个线程。

四.扩展内容

1.重谈pthread_create函数

前面提到过pthread_create函数的第四个参数和返回值都是void*类型,采用该类型主要是通过泛型的思想,适配任何指针类型。

其中pthread_create函数的第四个参数和返回值除了传递普通的内置类型(比如整形,字符串型等),还可以传递自定义类型(类和对象)

通过一段代码来测试:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;class Request{
public:Request(int start, int end, const string &threadname):_start(start),_end(end),_threadname(threadname){}
public:int _start;int _end;string _threadname;
};class Response{
public:Response(int result, int exitcode):_result(result),_exitcode(exitcode){}
public:int _result;int _exitcode;
};void *sumcount(void *args){Request *rq = static_cast<Request *>(args);Response *rsp = new Response(0, 0);for (int i = rq->_start; i <= rq->_end;i++){rsp->_result += i;}delete rq;pthread_exit(static_cast<void *>(rsp));
}int main(){pthread_t tid;Request *rq = new Request(1, 100, "thread 1");// 主线程创建一个新线程pthread_create(&tid, nullptr, sumcount, rq);// 主线程回收新线程void *retval;pthread_join(tid, &retval);Response *rsp = static_cast<Response *>(retval);cout << "rsp->result: " << rsp->_result << " rsp->exitcode: " << rsp->_exitcode << endl;delete rsp;return 0;
}

在这里插入图片描述

在上面的测试代码中,传递指针时,将自定义类型(rq)的指针强制转换为void*;然后在线程函数中,将void*强制转换为原始类型。

自定义类型的对象都是通过malloc或者new在堆上开辟空间的。

而堆空间也是被所有线程共享的,但是共享堆空间不等于自动共享数据

  • 堆空间的共享性:所有线程共享同一进程的堆内存区域,但堆上的数据必须通过指针来定位,需要明确直到其地址才能访问;
  • 数据的隔离性:虽然所有线程共享堆空间,但是线程之间默认不知道对方在堆中创建了哪些数据对象。

所以给线程的执行函数传参时,也可以传入自定义类型的对象的指针。

2.C++11线程库

这里先简单的了解即可,之后学C++11时会再重点讲解更详细的使用

C++11的线程库简单使用:

#include <iostream>
#include <unistd.h>
#include <string>
#include <thread>
using namespace std;void threadrun(){while(true){cout << "I am a new thread for C++" << endl;sleep(1);}
}int main(){// C++11的线程库thread t1(threadrun);t1.join();return 0;
}

C++11里的线程库本质上还是封装的原生线程库,所以使用C++11的线程库编译时还是需要带上-lpthread选项,此外还要带上-std=c++11选项

在这里插入图片描述

3.线程栈结构

每个线程在运行时都要有自己独立的栈结构,因为每一个线程都会有自己的调用链,也就注定了每一个线程都要有一个调用链对应的独立栈帧结构,这个栈结构会保存任意一个执行流在运行过程中所有的临时变量,比如压栈时传参形成的临时变量;返回时的返回值以及地址,包括线程自己在函数中定义的临时变量,所以每个线程都要有自己独立的栈结构。

其中主线程直接使用地址空间中提供的的栈结构,这就是系统真正意义上的进程

除了主线程外,其他线程的独立栈结构,都在共享区,具体来说是在pthread库中,每一个线程都有一个线程控制块tcb(由线程库维护),线程的栈结构就存储在tcb中,而tcb的起始地址就是线程的tid

上面讲解线程创建时函数的第一个参数线程ID,就是这个线程的tid地址。后续线程的所有操作都是根据这个线程ID来实现的。

线程库中还提供了pthread_self函数,可以获取线程自身的ID:

pthread_t pthread_self(void);

在这里插入图片描述

多线程测试栈区独立:

#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>
using namespace std;#define NUM 3class threadData{
public:threadData(int number):threadname("thread-"+to_string(number)){}
public:string threadname;
};string toHex(pthread_t tid){char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", tid);return buffer;
}// 所有线程都会调用这个函数
void *threadRoutine(void *args){threadData *td = static_cast<threadData *>(args);int i = 0;// 每个线程都创建一个test_i变量int test_i = 0;    while (i < 5){cout << "pid: " << getpid() << ", tid: " << toHex(pthread_self())<< ", threadname: " << td->threadname<< ", test_i: " << test_i << ", &test_i: " << toHex((pthread_t)&test_i) << endl;sleep(1);i++;test_i++;}delete td;return nullptr;
}int main(){// 创建多线程vector<pthread_t> tids;for (int i = 0; i < NUM; i++){pthread_t tid;threadData *td = new threadData(i);  // 在堆区创建pthread_create(&tid, nullptr, threadRoutine, td);tids.push_back(tid);sleep(1);}for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;
}

在这里插入图片描述

根据上面的测试就可以证明,每个线程都有自己的独立栈结构

如果某个线程想访问另一个线程栈区上的变量,也是可以实现的:

#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>
using namespace std;#define NUM 3int *p = nullptr;class threadData{
public:threadData(int number):threadname("thread-"+to_string(number)){}
public:string threadname;
};string toHex(pthread_t tid){char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", tid);return buffer;
}// 所有线程都会调用这个函数
void *threadRoutine(void *args){threadData *td = static_cast<threadData *>(args);int i = 0;int test_i = 0;    // 该变量在每个线程的栈区创建if(td->threadname=="thread-2"){p = &test_i;}while (i < 5){cout << "pid: " << getpid() << ", tid: " << toHex(pthread_self())<< ", threadname: " << td->threadname<< ", test_i: " << test_i << ", &test_i: " << &test_i << endl;sleep(1);i++;test_i++;}delete td;return nullptr;
}int main(){// 创建多线程vector<pthread_t> tids;for (int i = 0; i < NUM; i++){pthread_t tid;threadData *td = new threadData(i);  // 在堆区创建pthread_create(&tid, nullptr, threadRoutine, td);tids.push_back(tid);sleep(1);}sleep(1);cout << "main thread get a local value,val: " << *p << ", &val: " << p << endl;for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;
}

在这里插入图片描述

所有线程本来就共享代码区,全局变量区,堆区,共享区等,而对于线程独立的栈结构上的数据,也是可以被其他线程看到并访问的,所以线程和线程之间,几乎没有秘密。

4.线程局部存储

全局变量可以被所有线程看到并访问的

#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>
using namespace std;#define NUM 3//int *p = nullptr;
int g_val = 0;class threadData{
public:threadData(int number):threadname("thread-"+to_string(number)){}
public:string threadname;
};string toHex(pthread_t tid){char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", tid);return buffer;
}// 所有线程都会调用这个函数
void *threadRoutine(void *args){threadData *td = static_cast<threadData *>(args);int i = 0;//int test_i = 0;    // 该变量在每个线程的栈区创建// if(td->threadname=="thread-2"){//     p = &test_i;// }while (i < 5){cout << "pid: " << getpid() << ", tid: " << toHex(pthread_self())<< ", threadname: " << td->threadname<< ", g_val: " << g_val << ", &g_val: " << &g_val << endl;sleep(1);i++;//test_i++;g_val++;}delete td;return nullptr;
}int main(){// 创建多线程vector<pthread_t> tids;for (int i = 0; i < NUM; i++){pthread_t tid;threadData *td = new threadData(i);  // 在堆区创建pthread_create(&tid, nullptr, threadRoutine, td);tids.push_back(tid);sleep(1);}sleep(1);//cout << "main thread get a local value,val: " << *p << ", &val: " << p << endl;for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;
}

在这里插入图片描述

在上面的测试代码中,g_val是一个全局变量,被所有线程共享访问,所以这个g_val其实就是一个共享资源。

如果线程想要一个私有的全局变量,如何实现?

直接在定义的全局变量之前加__thread即可:

__thread int g_val=0;

在这里插入图片描述

每一个线程都访问的是同一个全局变量,但是每一个全局变量对于每一个线程来讲,都是各自私有一份,这种技术就是线程的局部存储

__thread不是C语言或C++的关键字,而是编译器提供的一个编译选项,编译的时候会默认将这个g_val变量给每一个线程在独立的栈结构上申请一份空间。

注意点:

__thread定义线程的局部存储变量时只能用来定义内置类型,不能用来修饰自定义类型:

在这里插入图片描述

5.分离线程

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏问题;
  • 但是如果线程等待时,并不关心线程的返回值,此时join就是一种负担,这个时候,我们可以使用pthread_detach函数分离线程,告诉系统当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离(比如主线程使某个线程分离),也可以是线程自己分离(在线程的执行函数中调用pthread_detach(pthread_self()))。

注意:joinable和分离是冲突的,一个线程不能既是joinable又是分离的

pthread_joinpthread_detach两个函数不能对同一个线程使用。

以上就是关于线程控制部分的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!

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

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

相关文章

More Effective C++学习笔记

条款1 指针与引用的区别 条款2 尽量使用C风格的类型转换 条款3 不要对数组使用多态 条款4 避免无用的缺省构造函数 条款5 谨慎定义类型转换函数 条款6 自增(increment)、自减(decrement)操作符前缀形式与后缀形式的区别 条款7 不要重载“&&”,“||”, 或“,” 条款8 理…

先知AIGC超级工场,撬动运营效率新杠杆

北京先智先行科技有限公司&#xff0c;作为行业内的重要参与者&#xff0c;拥有“先知大模型”、“先行AI商学院”以及“先知AIGC超级工场”这三款旗舰产品。这些产品在不同领域发挥着关键作用&#xff0c;尤其是先知AIGC超级工场&#xff0c;正悄然改变着内容创作与产品推广的…

十一岁少年叶珉雪用艺术点亮公益之路 个人原创公益演唱会传递大爱与担当

4月29日晚&#xff0c;"韶华映雪益路同行"叶珉雪个人原创公益演唱会在广东碧桂园学校歌剧院圆满落幕。 这场由该校美育成果浇灌出的艺术盛宴&#xff0c;生动诠释了广东碧桂园学校育人理念。11岁的叶珉雪以超越年龄的艺术掌控力&#xff0c;呈现了一场融合歌唱、舞蹈…

【深度学习基础】:VGG实战篇(图像风格迁移)

文章目录 前言style transfer原理原理解析损失函数 style transfer代码效果图 fast style transfer 代码效果图 前言 本篇来带大家看看VGG的实战篇&#xff0c;这次来带大家看看计算机视觉中一个有趣的小任务&#xff0c;图像风格迁移。 可运行代码位于&#xff1a; Style_tr…

python爬虫基础:requests库详解与案例

1.Requests模块的使用 requests模块的介绍与安装 作用&#xff1a;发送网络请求&#xff0c;返回响应数据。 中文文档&#xff1a;https://requests.readthedocs.io/projects/cn/zh_CN/latest/ 对于爬虫任务&#xff0c;使用 requests模块基本能够解决绝大部分的数据抓取的…

Spring 容器相关的核心注解​

以下是 Spring 容器中用于 ​​Bean 管理、依赖注入、配置控制​​ 的关键注解&#xff0c;按功能分类说明&#xff1a; ​​1. Bean 声明与注册​​ 注解作用示例​​Component​​通用注解&#xff0c;标记一个类为 Spring Bean&#xff08;自动扫描注册&#xff09; Compo…

C与指针5——字符串合集

常用函数 1、拷贝、长度、比较 size_t strlen();\\返回无符号整形 char* strcpy();char* strncpy();\\拷贝 int strcmp();int strncmp();\\比较 char* strcat();char* strncat();\\连接2、查找 char* strchr(const char * st,int ch);\\找字符第一次出现的位置 char* strrch…

论软件需求管理

目录 摘要&#xff08;300~330字&#xff09; 正文&#xff08;2000~2500字&#xff0c;2200字为宜&#xff09; 背景介绍&#xff08;500字做左右&#xff09; 论点论据&#xff08;1500字做左右&#xff09; 收尾&#xff08;200字左右&#xff09; 注&#xff1a;本篇论…

[特殊字符] 如何在比赛前调整到最佳状态:科学与策略结合的优化指

&#x1f9e0; 概述 在竞技体育中&#xff0c;赛前状态的调整对比赛结果起着决定性作用。所谓“最佳状态”&#xff0c;不仅指生理上的巅峰表现&#xff0c;更包括心理、认知、营养和恢复等多方面的协同优化。本文结合运动科学、心理学和营养学的研究成果&#xff0c;探讨赛前…

一种实波束前视扫描雷达目标二维定位方法——论文阅读

一种实波束前视扫描雷达目标二维定位方法 1. 专利的研究目标与实际问题意义2. 专利提出的新方法、模型与公式2.1 运动平台几何建模与回波信号构建2.1.1 距离历史建模2.1.2 回波信号模型2.2 距离向运动补偿技术2.2.1 匹配滤波与距离压缩2.3 加权最小二乘目标函数2.3.1 方位向信号…

基于 Spring Boot 瑞吉外卖系统开发(八)

基于 Spring Boot 瑞吉外卖系统开发&#xff08;八&#xff09; 自动填充公共字段 MyBatis-Plus公共字段自动填充&#xff0c;也就是在插入或者更新的时候为指定字段赋予指定的值&#xff0c;使用它的好处就是可以统一对这些字段进行处理&#xff0c;降低了冗余代码的数量。本…

【前端】从零开始的搭建结构(技术栈:Node.js + Express + MongoDB + React)book-management

项目路径总结 后端结构 server/ ├── controllers/ # 业务逻辑 │ ├── authController.js │ ├── bookController.js │ ├── genreController.js │ └── userController.js ├── middleware/ # 中间件 │ ├── authMiddleware…

【RAG】向量?知识库的底层原理:向量数据库の技术鉴赏 | HNSW(导航小世界)、LSH、K-means

一、向量化表示的核心概念 1.1 特征空间与向量表示 多维特征表示&#xff1a;通过多个特征维度&#xff08;如体型、毛发长度、鼻子长短等&#xff09;描述对象&#xff0c;每个对象对应高维空间中的一个坐标点&#xff0c;来表示狗这个对象&#xff0c;这样可以区分出不同种…

如何用CSS实现HTML元素的旋转效果

原文&#xff1a;如何用CSS实现HTML元素的旋转效果 | w3cschool笔记 &#xff08;本文为科普文章&#xff0c;请勿标记为付费&#xff09; 在网页制作中&#xff0c;为 HTML 元素设置旋转效果可使其更灵动&#xff0c;提升用户体验。本文将深入浅出地介绍如何利用 CSS 实现 H…

Spark集群搭建之Yarn模式

配置集群 1.上传并解压spark-3.1.2-bin-hadoop3.2.tgz&#xff0c;重命名解压之后的目录为spark-yarn。 2. 修改一下spark的环境变量&#xff0c;/etc/profile.d/my_env.sh 。 # spark 环境变量 export SPARK_HOME/opt/module/spark-yarn export PATH$PATH:$SPARK_HOME/bin:$SP…

xLua笔记

Generate Code干了什么 肉眼可见的&#xff0c;在Asset文件夹生成了XLua/Gen文件夹&#xff0c;里面有一些脚本。然后对加了[CSharpCallLua]的变量寻找引用&#xff0c;发现它被XLua/Gen/DelegatesGensBridge引用了。也可以在这里查哪些类型加了[CSharpCallLua]。 public over…

【tcp连接windows redis】

tcp连接windows redis 修改redis.conf 修改redis.conf bind * -::*表示禁用保护模式&#xff0c;允许外部网络连接 protected-mode no

【序列贪心】摆动序列 / 最长递增子序列 / 递增的三元子序列 / 最长连续递增序列

⭐️个人主页&#xff1a;小羊 ⭐️所属专栏&#xff1a;贪心算法 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 摆动序列最长递增子序列递增的三元子序列最长连续递增序列 摆动序列 摆动序列 贪心策略&#xff1a;统计出所有的极大值和极小…

STM32F103C8T6使用MLX90614模块

首先说明&#xff1a; 1.SMBus和I2C的区别 我曾尝试用江科大的I2C底层去直接读取该模块&#xff0c;但是无法成功&#xff0c;之后AI生成的的代码也无法成功。 思来想去最大的可能就是SMBus这个协议的问题&#xff0c;根据百度得到的结果如下&#xff1a; SMBus和I2C的区别 链…

tp5 php获取农历年月日干支甲午

# 切换为国内镜像源 composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/# 再次尝试安装 composer require overtrue/chinese-calendar核心写法一个农历转公历&#xff0c;一个公历转农历 农历闰月可能被错误标记&#xff08;例如 闰四月 应表示…