2024-02-27-物联网系统编程(8-线程)
8. 线程
8.1 概述
8.1.1 线程
每个进程都拥有自己的数据段、代码段和堆栈段,这就造成进程在进行创建、切换、撤销操作时,需要较大的系统开销。为了减少系统开销,从进程中演化出了线程。
线程存在于进程中,共享进程的资源。
线程是进程中的独立控制流,由环境(包括寄存器组和程序计数器)和一系列的执行指令组成。
每个进程有一个地址空间和一个控制线程。

8.1.2 线程与进程
调度:
线程是 CPU 调度和分派的基本单位。
拥有资源:
进程是系统中程序执行和资源分配的基本单位。
线程一般不拥有资源(除了必不可少的程序计数器、寄存器和栈),但它可以去访问其所属进程的资源,如进程代码段、数据段以及系统资源(已打开的文件,I/0设备等)。
系统开销
同一个进程中的多个线程可共享同一地址空间,因此它们之间的同步和通信的实现也变得比较容易。在进程切换时候,涉及到整个当前进程 CPU环境的保存以及新被调度运行的进程的 CPU环境的设置;
而线程切换只需要保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作,从而能更有效地使用系统资源和提高系统的吞吐量。
并发性:
不仅进程间可以并发执行,而且在一个进程中的多个线程之间也可以并发执行。
总结:
- 一般把线程称之为轻量级的进程
- 一个进程可以创建多个线程,多个线程共享一个进程的资源
- 每一个进程创建的时候系统会给其4G虚拟内存,3G用户空间是私有的,所以进程切换时,用户空间也会切换,所以会增加系统开销,而一个进程中的多个线程共享一个进程的资源,所以线程切换时不用切换这些资源,效率会更高
- 线程的调度机制跟进程是一样的,多个线程来回切换运行
8.1.3 多线程的用处
多任务程序的设计
一个程序可能要处理不同应用,要处理多种任务,如果开发不同的进程来处理,系统开销很大,数据共享,程序结构都不方便,这时可使用多线程编程方法。
并发程序的设计
一个任务可能分成不同的步骤去完成,这些不同的步骤之间可能是松散耦合,可能通过线程的互斥,同步并发完成。这样可以为不同的任务步骤建立线程。
网络程序设计
为提高网络的利用效率,我们可能使用多线程,对每个连接用一个线程去处理。
在多 CPU 系统中,实现真正的并行
8.2 线程的基本操作
就像每个进程都有一个进程号一样,每个线程也有一个线程号。
- 进程号在整个系统中是唯一的,但线程号不同,线程号只在它所属的进程环境中有效。
- 进程号用
pid_t
数据类型表示,是一个非负整数;线程号则用pthread_t
数据类型来表示。 - 有的系统在实现
pthread_t
的时候,用一个结构体来表示,所以在可移植的操作系统实现不能把它做为整数处理。
8.2.1 线程的创建
#include <pthread.h> int pthread create(pthread_t *thread, const pthread attr_t *attr,void *(*start _routine)(void*),void *arg); 功能:创建一个新的子线程 参数; thread:当前创建的线程id attr:线程的属性,设置为NULL表示以默认的属性创建 start_routine:线程处理函数,如果当前函数执行完毕,则子线程也执行完毕; arg:给线程处理函数传参用的返回值: 成功: 0 失败: 非a
注意事项:
- 与fork不同,
pthread_create
创建的线程不与父线程在同一点开始运行,而是从指定的函数开始运行,该函数运行完后,该线程也就退出了。 - 线程依赖进程存在的,如果创建线程的进程结束了,线程也就结束了
- 线程函数的程序在
pthread
库中,故链接时要加上参数-lpthread
#include <stdio.h> #include <stdlib.h> #include <pthread.h> // 由于线程库原本不是系统本身的,所以链接时需要手动链接库文件 gcc *.c -lpthread void *thread_fun(void *arg) { printf("子线程正在运行\n"); } int main(int argc, char const *argv[]) { // 通过pthread_create创建子线程 printf("主控函数正在执行\n"); pthread_t thread; if (pthread_create(&thread, NULL, thread_fun, NULL) != 0) { perror("fail to create thread"); exit(1); } // 由于进程结束后,进程中所有线程都会强制退出 while (1) { } return 0; }
输出结果
主控函数正在执行 子线程正在运行
8.2.2 线程调度机制的验证
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> // 由于线程库原本不是系统本身的,所以链接时需要手动链接库文件 gcc *.c -lpthread // 一个进程中的多个进程顺序是不确定的,没有先后顺序可言 // 多线程执行时,和进程一样,是来回切换的 void *thread_fun1(void *arg) { printf("子线程1正在运行\n"); sleep(1); printf("*******************************"); } void *thread_fun2(void *arg) { printf("子线程2正在运行\n"); sleep(1); printf("-------------------------------"); } int main(int argc, char const *argv[]) { // 通过pthread_create创建子线程 printf("主控函数正在执行\n"); pthread_t thread; if (pthread_create(&thread, NULL, thread_fun1, NULL) != 0) { perror("fail to create thread"); exit(1); } if (pthread_create(&thread, NULL, thread_fun2, NULL) != 0) { perror("fail to create thread"); exit(1); } // 由于进程结束后,进程中所有线程都会强制退出 while (1) { } return 0; }
执行结果
spider@ubuntu:~/C/08线程$ ./output/01_pthread_create02 主控函数正在执行 子线程2正在运行 子线程1正在运行 ^C spider@ubuntu:~/C/08线程$ ./output/01_pthread_create02 主控函数正在执行 子线程1正在运行 子线程2正在运行 ^C spider@ubuntu:~/C/08线程$ ./output/01_pthread_create02 主控函数正在执行 子线程1正在运行 子线程2正在运行 ^C spider@ubuntu:~/C/08线程$ ./output/01_pthread_create02 主控函数正在执行 子线程1正在运行 子线程2正在运行
8.2.3 线程处理函数的传参问题
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> // 由于线程库原本不是系统本身的,所以链接时需要手动链接库文件 gcc *.c -lpthread // 线程处理函数可以认为就是一个普通的全局函数,只不过与普通函数最大的区别: // 线程处理函数是并行执行,来回交替执行,但是普通函数一定按照顺序一个一个执行 int num = 100; void *thread_fun1(void *arg) { printf("子线程1,num = %d\n", num); num++; } void *thread_fun2(void *arg) { printf("子线程2,num = %d\n", num); num++; } int main(int argc, char const *argv[]) { // 通过pthread_create创建子线程 printf("主控函数正在执行\n"); pthread_t thread; if (pthread_create(&thread, NULL, thread_fun1, NULL) != 0) { perror("fail to create thread"); exit(1); } if (pthread_create(&thread, NULL, thread_fun2, NULL) != 0) { perror("fail to create thread"); exit(1); } // 由于进程结束后,进程中所有线程都会强制退出 while (1) { } return 0; }
输出结果
主控函数正在执行 子线程1,num = 100 子线程2,num = 101 ^C
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> // 由于线程库原本不是系统本身的,所以链接时需要手动链接库文件 gcc *.c -lpthread // 线程处理函数可以认为就是一个普通的全局函数,只不过与普通函数最大的区别: // 线程处理函数是并行执行,来回交替执行,但是普通函数一定按照顺序一个一个执行 int num = 100; void *thread_fun1(void *arg) { printf("子线程1,num = %d\n", num); num++; // 定义变量接受参数 int n = *(int *)arg; printf("1 n = %d\n", n); // 可以修改子进程1中变量地址中的值,这样子进程2也会读取这个值,发生修改 *(int *)arg = 111; } void *thread_fun2(void *arg) { printf("子线程2,num = %d\n", num); num++; int n = *(int *)arg; printf("2 n = %d\n", n); } int main(int argc, char const *argv[]) { // 通过pthread_create创建子线程 printf("主控函数正在执行\n"); pthread_t thread; int a = 666; // 通过第四个参数向函数thread_fun1中传值,第四个参数是(void *)类型,所以传入地址 if (pthread_create(&thread, NULL, thread_fun1, (void *)&a) != 0) { perror("fail to create thread"); exit(1); } // 通过第四个参数向函数thread_fun2中传值,第四个参数是(void *)类型,所以传入地址 if (pthread_create(&thread, NULL, thread_fun2, (void *)&a) != 0) { perror("fail to create thread"); exit(1); } // 由于进程结束后,进程中所有线程都会强制退出 while (1) { } return 0; }
输出结果
主控函数正在执行 子线程1,num = 100 1 n = 666 子线程2,num = 101 2 n = 111
8.2.4 线程等待
#include <pthread.h> int pthread join(pthread_t thread, void **retval); 功能:阻塞等待一个子线程的退出,可以接收到某一个子线程调用pthread_exit时设置的退出状态值 参数: thread:指定线程的id retval:保存子线程的退出状态值,如果不接受则设置为NULL返回值: 成功: 0 失败: 非0
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void *thread_fun1(void *arg) { printf("子线程1正在执行\n"); } int main(int argc, char const *argv[]) { // 通过pthread_create创建子线程 printf("主控函数正在执行\n"); pthread_t thread; // 通过第四个参数向函数thread_fun1中传值,第四个参数是(void *)类型,所以传入地址 if (pthread_create(&thread, NULL, thread_fun1,NULL) != 0) { perror("fail to create thread"); exit(1); } // 由于进程结束后,进程中所有线程都会强制退出 return 0; }
输出结果
主控函数正在执行 # 主控函数直接执行就退出了,没有时间给子线程执行
此时,需要使用pthread_join
函数阻塞主控函数,等待子线程执行完后再退出
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void *thread_fun1(void *arg) { printf("子线程1正在执行\n"); sleep(1); printf("子线程1即将退出\n"); } int main(int argc, char const *argv[]) { // 通过pthread_create创建子线程 printf("主控函数正在执行\n"); pthread_t thread; // 通过第四个参数向函数thread_fun1中传值,第四个参数是(void *)类型,所以传入地址 if (pthread_create(&thread, NULL, thread_fun1, NULL) != 0) { perror("fail to create thread"); exit(1); } // 由于进程结束后,进程中所有线程都会强制退出 // 此时,使用pthread_join来阻塞子线程 if (pthread_join(thread, NULL) != 0) { perror("fail to pthread_join\n"); } printf("进程即将退出\n"); return 0; }
输出结果
主控函数正在执行 子线程1正在执行 子线程1即将退出 进程即将退出
子线程也可以退出时传参给主进程
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void *thread_fun1(void *arg) { static int num = 100; printf("子线程1正在执行\n"); sleep(3); printf("子线程1即将退出\n"); // 子线程如果需要返回退出状态,可以通过返回值或者通过pthread_exit函数 return (void *)# } int main(int argc, char const *argv[]) { // 通过pthread_create创建子线程 printf("主控函数正在执行\n"); pthread_t thread; // 通过第四个参数向函数thread_fun1中传值,第四个参数是(void *)类型,所以传入地址 if (pthread_create(&thread, NULL, thread_fun1, NULL) != 0) { perror("fail to create thread"); exit(1); } // 由于进程结束后,进程中所有线程都会强制退出 // 此时,使用pthread_join来阻塞子线程 // 定义一级指针变量,获取二级指针的值 int *num; if (pthread_join(thread, (void **)&num) != 0) { perror("fail to pthread_join\n"); } printf("ret_val = %d\n", *num); printf("进程即将退出\n"); return 0; }
输出结果
主控函数正在执行 子线程1正在执行 子线程1即将退出 ret_val = 100 进程即将退出
8.2.5 线程分离
线程的结合态和分离态
linux 线程执行和 windows 不同,pthread 有两种状态:可结合的(joinable)或者是分离的(detached),线程默认创建为可结合态;
如果线程是 joinable
,状态,当线程函数自己返回退出时或 pthread_exit
时都不会释放线程所占用堆栈和线程描述符(总计 8K多)。
只有当你调用了 pthread_join
之后这些资源才会被释放。
若是 detached 状态的线程,这些资源在线程函数退出时或pthread_exit
时自动会被释放,使用 pthread_detach
函数将线程设置为分离态。
#include <pthread.h> int pthread_detach(pthread_t, thread); 功能:使调用线程与当前进程分离,使其成为一个独立的线程,该线程终止时,系统将自动回收它的资源。 参数: thread: 指定的子线程的id; 返回值: 成功: 0 失败: 非0
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void *thread_fun1(void *arg) { printf("子线程1正在执行\n"); sleep(1); printf("子线程1即将退出\n"); } int main(int argc, char const *argv[]) { // 通过pthread_create创建子线程 printf("主控函数正在执行\n"); pthread_t thread; // 通过第四个参数向函数thread_fun1中传值,第四个参数是(void *)类型,所以传入地址 if (pthread_create(&thread, NULL, thread_fun1, NULL) != 0) { perror("fail to create thread"); exit(1); } // 由于进程结束后,进程中所有线程都会强制退出 // 此时,使用pthread_join来阻塞子线程 // 如果子线程是结合态,需要通过pthread_join回收子进程退出的资源 // 但是这个函数是一个阻塞函数,如果子线程不退出,就会导致当前进程无法继续执行,大大限制代码的运行效率 // 通过pthread_detach函数将子线程设置为分离态,实现不阻塞,也可以回收资源 if (pthread_detach(thread) != 0) { // perror("fail to pthread_join\n"); exit(1); } #if 0 // 如果子线程设置了分离态,就不需要pthread_join了 if (pthread_join(thread, NULL) != 0) { perror("fail to pthread_join\n"); exit(1); } #endif while (1) { printf("hello world\n"); sleep(1); } printf("进程即将退出\n"); return 0; }
输出结果
主控函数正在执行 hello world 子线程1正在执行 子线程1即将退出 hello world hello world
8.2.6 线程退出
在进程中我们可以调用 exit
函数或 _exit
函数来结束进程,在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流。
- 线程从执行函数中返回
- 线程调用
pthread_exit
退出线程 - 线程可以被同一进程中的其它线程取消
#include <pthread.h> void pthread exit(void *retval); 功能:退出正在执行的线程 参数: retval:当前线程的退出状态值, 这个值可以被调用pthread_join函数的线程接收到 返回值: 无
注意:
- 一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放。
- 如果要释放资源,结合态需要通过
pthread_join
函数,分离态则自动释放
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void *thread_fun1(void *arg) { printf("子线程1正在执行\n"); static char buf[] = "the thread has quited\n"; int i; for (int i = 0; i < 10; i++) { printf("*********************\n"); sleep(1); if (i == 5) { // 通过pthread_exit函数退出线程 // 参数设定为NULL,无返回值 // 参数设定返回值 printf("buf = %s\n", buf); pthread_exit(buf); } } printf("子线程1即将退出\n"); } int main(int argc, char const *argv[]) { // 通过pthread_create创建子线程 printf("主控函数正在执行\n"); pthread_t thread; // 通过第四个参数向函数thread_fun1中传值,第四个参数是(void *)类型,所以传入地址 if (pthread_create(&thread, NULL, thread_fun1, NULL) != 0) { perror("fail to create thread"); exit(1); } // 接受线程的退出返回值 char *str; pthread_join(thread, (void **)&str); printf("str = %s\n", str); printf("进程即将退出\n"); return 0; }
输出结果
主控函数正在执行 子线程1正在执行 ********************* ********************* ********************* ********************* ********************* ********************* buf = the thread has quited str = the thread has quited 进程即将退出
8.2.7 线程的取消
取消线程是指取消一个正在执行线程的操作。
#include <pthread.h> int pthread cancel(pthread t thread); 功能: 取消线程。 参数: thread:目标线程 D 返回值: 成功: 返回 0; 失败: 返回出错编号。
pthread cancel 函数的实质是发信号给目标线程 thread,使目标线程退出。
此函数只是发送终止信号给目标线程,不会等待取消目标线程执行完才返回。
然而发送成功并不意味着目标线程一定就会终止,线程被取消时,线程的取消属性会决定线程能否被取消以及何时被取消。
线程的取消状态: 即线程能不能被取消线程;
取消点:即线程被取消的地方;
线程的取消类型:在线程能被取消的状态下,是立马被取消结束还是执行到取消点的时候被取消;
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void *thread_fun1(void *arg) { printf("子线程1正在执行\n"); static char buf[] = "the thread has quited\n"; int i; for (int i = 0; i < 10; i++) { sleep(1); printf("*********************\n"); } printf("子线程1即将退出\n"); } int main(int argc, char const *argv[]) { // 通过pthread_create创建子线程 printf("主控函数正在执行\n"); pthread_t thread; // 通过第四个参数向函数thread_fun1中传值,第四个参数是(void *)类型,所以传入地址 if (pthread_create(&thread, NULL, thread_fun1, NULL) != 0) { perror("fail to create thread"); exit(1); } // 通过pthread_cancel 函数取消另一个线程 sleep(3); pthread_cancel(thread); printf("进程即将退出\n"); return 0; }
输出结果
主控函数正在执行 子线程1正在执行 ********************* ********************* ********************* 进程即将退出
线程的取消状态
在Linux系统下,线程默认可以被取消。
编程时可以通过pthread setcancelstate
函数设置线程是否可以被取消。
#include <pthread.h> int pthread setcancelstate(int state,int *oldstate); 功能:设置线程是否被取消; 参数: state:新的状态 PTHREAD_CANCEL_DISABLE:不可以被取消 PTHREAD_CANCEL_ENABLE:可以被取消 oldstate:保存调用线程原来的可取消状态的内存地址返回值: 成功: 0 失败: 非0
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void *thread_fun1(void *arg) { printf("子线程1正在执行\n"); // 通过pthread_setcancelstate设置取消的状态 // 设置可以取消,默认可以取消 // pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL); // 设置不可取消 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL); for (int i = 0; i < 10; i++) { sleep(1); printf("*********************\n"); } printf("子线程1即将退出\n"); } int main(int argc, char const *argv[]) { // 通过pthread_create创建子线程 printf("主控函数正在执行\n"); pthread_t thread; // 通过第四个参数向函数thread_fun1中传值,第四个参数是(void *)类型,所以传入地址 if (pthread_create(&thread, NULL, thread_fun1, NULL) != 0) { perror("fail to create thread"); exit(1); } // 通过pthread_cancel 函数取消另一个线程 sleep(3); pthread_cancel(thread); pthread_join(thread,NULL); printf("进程即将退出\n"); return 0; }
输出结果
主控函数正在执行 子线程1正在执行 ********************* ********************* ********************* ********************* ********************* ********************* ********************* ********************* ********************* ********************* 子线程1即将退出 进程即将退出
线程的取消点
线程被取消后,该线程并不是马上终止,默认情况下线程执行到消点时才能被终止。编程时可以通过pthread_testcancel
函数设置线程的取消点。
void pthread testcancel(void):
当别的线程取消调用此函数的线程时候,被取消的线程执行到此函数时结束。
#include <pthread.h> void pthread testcancel(void); 功能:设置线程的取消点; 参数:无 返回值:无
线程的取消类型
线程被取消后,该线程并不是马上终止,默认情况下线程执行到消点时才能被终止。编程时可以通过pthreadr_setcanceltype
,函数设置线程是否可以立即被取消。
#include <pthread.h> int pthread setcanceltype(int type, int *oldtype); 功能:设置线程是否可以立即被取消; 参数: type:类型 PTHREAD_CANCEL_ASYNCHRONOUS:立即取消; PTHREAD_CANCEL_DEFERRED:不立即被取消; oldtype:保存调用线程原来的可取消类型的内存地址; 返回值: 成功:0 失败:非0
8.2.8 线程退出清理函数
和进程的退出清理一样,线程也可以注册它退出时要调用的函数,这样的函数称为线程清理处理程序(threadcleanup handler)
.
注意:
- 线程可以建立多个清理处理程序。
- 处理程序在栈中,故它们的执行顺序与它们注册时的顺序相反。
pthread_cleanup_push
#include <pthread.h> void pthread_cleanup_push(void(* routine)(void *),void *arg); 功能:将清除函数压栈,即注册清理函数; 参数: routine:线程清理函数的指针。 arg:传给线程清理函数的参数; 返回值:无
pthread_cleanup_pop
#include <pthread.h> void pthread_cleanup_pop(int execute); 功能:将清除函数弹栈,即删除清理函数; 参数: execute:线程清理函数执行标志位。 非0,弹出清理函数,执行清理函数。 0,弹出清理函数,不执行清理函数; 返回值:无
案例1:验证线程调用pthread_exit函数时,系统自动调用线程清理函数
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <string.h> void mycleanup(void *arg) { printf("clear up for = %s\n", (char *)arg); free((char *)arg); } void *thread(void *arg) { char *ptr = NULL; // 建立线程清理程序 printf("this is new thread\n"); ptr = (char *)malloc(100); pthread_cleanup_push(mycleanup, (void *)(ptr)); bzero(ptr, 100); strcpy(ptr, "memory from malloc"); printf("before exit \n"); sleep(3); pthread_exit(NULL); // 注意push与pop必须配对使用,及时pop执行不到 printf("before pop \n"); pthread_cleanup_pop(1); } int main(int argc, char const *argv[]) { pthread_t tid; pthread_create(&tid, NULL, thread, NULL); pthread_join(tid, NULL); printf("process is dying\n"); return 0; }
输出结果
this is new thread before exit clear up for = memory from malloc process is dying
案例2:验证线程被取消时,系统自动调用线程清理函数
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <string.h> void mycleanup(void *arg) { printf("clear up for = %s\n", (char *)arg); free((char *)arg); } void *thread(void *arg) { char *ptr = NULL; // 建立线程清理程序 printf("this is new thread\n"); ptr = (char *)malloc(100); pthread_cleanup_push(mycleanup, (void *)(ptr)); bzero(ptr, 100); strcpy(ptr, "memory from malloc"); printf("before exit \n"); sleep(10); // pthread_exit(NULL); // 注意push与pop必须配对使用,及时pop执行不到 printf("before pop \n"); pthread_cleanup_pop(1); } int main(int argc, char const *argv[]) { pthread_t tid; pthread_create(&tid, NULL, thread, NULL); sleep(5); pthread_cancel(tid); pthread_join(tid, NULL); printf("process is dying\n"); return 0; }
输出结果
this is new thread before exit clear up for = memory from malloc process is dying
案例3 :验证调用pthread_cleanup_pop函数时,系统自动调用线程清理函数
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <string.h> void mycleanup1(void *arg) { printf("clear up for fun1\n"); free((char *)arg); } void mycleanup2(void *arg) { printf("clear up for fun2\n"); free((char *)arg); } void *thread(void *arg) { char *ptr = NULL; // 建立线程清理程序 printf("this is new thread\n"); ptr = (char *)malloc(100); pthread_cleanup_push(mycleanup1, (void *)(ptr)); pthread_cleanup_push(mycleanup2, NULL); bzero(ptr, 100); strcpy(ptr, "memory from malloc"); printf("before exit \n"); sleep(5); // pthread_exit(NULL); // 注意push与pop必须配对使用,及时pop执行不到 printf("before pop \n"); pthread_cleanup_pop(1); printf("before pop \n"); pthread_cleanup_pop(1); } int main(int argc, char const *argv[]) { pthread_t tid; pthread_create(&tid, NULL, thread, NULL); sleep(5); pthread_cancel(tid); pthread_join(tid, NULL); printf("process is dying\n"); return 0; }
输出结果(栈先进后出,先清理fun2,再清理fun1)
this is new thread before exit before pop clear up for fun2 before pop clear up for fun1 process is dying
本文来自博客园,作者:Yasuo_Hasaki,转载请注明原文链接:https://www.cnblogs.com/hasaki-yasuo/p/18039807
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步