Linux多线程详解
Linux多线程详解
- 一、Linux多线程概念
- 1.1 什么是线程
- 1.2 进程和线程
- 1.3 进程的多个线程共享
- 1.4 进程和线程的关系
- 二、Linux线程控制
- 2.1 POSIX线程库
- 2.2 线程创建
- 2.3 获取线程ID pthread_self
- 2.4 线程等待pthread_join
- 2.5 线程终止
- 2.6 线程栈 && pthread_t
- 2.7 线程的局部存储
- 2.8 分离线程pthread_detach
- 三、线程的优缺点
- 3.1 线程的优点
- 3.2 线程的缺点
- 四、线程异常
- 五、线程用途
一、Linux多线程概念
1.1 什么是线程
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
- 每个进程至少都有一个执行线程。
- 线程在进程内部运行,本质是在进程地址空间内运行。
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更轻量化。
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
需要明确的是,一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建,虚拟地址和物理地址就是通过页表建立映射的。
每个进程都有自己独立的进程地址空间和独立的页表,也就意味着所有进程在运行时本身就具有独立性。
但如果我们在创建“进程”时,只创建task_struct,并要求创建出来的task_struct和父task_struct共享进程地址空间和页表,那么创建的结果就是下面这样的:
此时我们创建的实际上就是四个线程:
- 其中每一个线程都是当前进程里面的一个执行流,也就是我们常说的“线程是进程内部的一个执行分支”。
- 同时我们也可以看出,线程在进程内部运行,本质就是线程在进程地址空间内运行,也就是说曾经这个进程申请的所有资源,几乎都是被所有线程共享的。
注意: 单纯从技术角度,这个是一定能实现的,因为它比创建一个原始进程所做的工作更轻量化了。
该如何重新理解之前的进程?
下面用蓝色方框框起来的内容,我们将这个整体叫做进程。
因此,所谓的进程并不是通过task_struct来衡量的,除了task_struct之外,一个进程还要有进程地址空间、文件、信号等等,合起来称之为一个进程。
现在我们应该站在内核角度来理解进程:承担分配系统资源的基本实体,叫做进程。
换言之,当我们创建进程时是创建一个task_struct、创建地址空间、维护页表,然后在物理内存当中开辟空间、构建映射,打开进程默认打开的相关文件、注册信号对应的处理方案等等。
而我们之前接触到的进程都只有一个task_struct,也就是该进程内部只有一个执行流,即单执行流进程,反之,内部有多个执行流的进程叫做多执行流进程。
在Linux中,站在CPU的角度,能否识别当前调度的task_struct是进程还是线程?
答案是不能,也不需要了,因为CPU只关心一个一个的独立执行流。无论进程内部只有一个执行流还是有多个执行流,CPU都是以task_struct为单位进行调度的。
单执行流进程被调度:
多执行流进程被调度:
因此,CPU看到的虽说还是task_struct,但已经比传统的进程要更轻量化了。
Linux下并不存在真正的多线程!而是用进程模拟的!
操作系统中存在大量的进程,一个进程内又存在一个或多个线程,因此线程的数量一定比进程的数量多,当线程的数量足够多的时候,很明显线程的执行粒度要比进程更细。
如果一款操作系统要支持真的线程,那么就需要对这些线程进行管理。比如说创建线程、终止线程、调度线程、切换线程、给线程分配资源、释放资源以及回收资源等等,所有的这一套相比较进程都需要另起炉灶,搭建一套与进程平行的线程管理模块。
因此,如果要支持真的线程一定会提高设计操作系统的复杂程度。在Linux看来,描述线程的控制块和描述进程的控制块是类似的,因此Linux并没有重新为线程设计数据结构,而是直接复用了进程控制块,所以我们说Linux中的所有执行流都叫做轻量级进程。
但也有支持真的线程的操作系统,比如Windows操作系统,因此Windows操作系统系统的实现逻辑一定比Linux操作系统的实现逻辑要复杂得多。
既然在Linux没有真正意义的线程,那么也就绝对没有真正意义上的线程相关的系统调用!
这很好理解,既然在Linux中都没有真正意义上的线程了,那么自然也没有真正意义上的线程相关的系统调用了。但是Linux可以提供创建轻量级进程的接口,也就是创建进程,共享空间,其中最典型的代表就是vfork函数。
vfork函数的功能就是创建子进程,但是父子共享空间,vfork的函数原型如下:
pid_t vfork(void);
vfork函数的返回值与fork函数的返回值相同:
- 给父进程返回子进程的PID。
- 给子进程返回0。
只不过vfork函数创建出来的子进程与其父进程共享地址空间,例如在下面的代码中,父进程使用vfork函数创建子进程,子进程将全局变量g_val由100改为了200,父进程休眠3秒后再读取到全局变量g_val的值。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int g_val = 100;
int main()
{pid_t id = vfork();if (id == 0){//childg_val = 200;printf("child:PID:%d, PPID:%d, g_val:%d\n", getpid(), getppid(), g_val);exit(0);}//fathersleep(3);printf("father:PID:%d, PPID:%d, g_val:%d\n", getpid(), getppid(), g_val);return 0;
}
可以看到,父进程读取到g_val的值是子进程修改后的值,也就证明了vfork创建的子进程与其父进程是共享地址空间的。
原生线程库pthread
在Linux中,站在内核角度没有真正意义上线程相关的接口,但是站在用户角度,当用户想创建一个线程时更期望使用thread_create这样类似的接口,而不是vfork函数,因此系统为用户层提供了原生线程库pthread。
原生线程库实际就是对轻量级进程的系统调用进行了封装,在用户层模拟实现了一套线程相关的接口。
因此对于我们来讲,在Linux下学习线程实际上就是学习在用户层模拟实现的这一套接口,而并非操作系统的接口。
1.2 进程和线程
进程是承担分配系统资源的基本实体,线程是调度的基本单位。
线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID。
- 一组寄存器。(存储每个线程的上下文信息)
- 栈。(每个线程都有临时的数据,需要压栈出栈)
- errno。(C语言提供的全局变量,每个线程都有自己的)
- 信号屏蔽字。
- 调度优先级。
1.3 进程的多个线程共享
因为是在同一个地址空间,因此所谓的代码段(Text Segment)、数据段(Data Segment)都是共享的:
- 如果定义一个函数,在各线程中都可以调用。
- 如果定义一个全局变量,在各线程中都可以访问到。
除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表。(进程打开一个文件后,其他线程也能够看到)
- 每种信号的处理方式。(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
- 当前工作目录。(cwd)
- 用户ID和组ID。
1.4 进程和线程的关系
进程和线程的关系如下图:
在此之前我们接触到的都是具有一个线程执行流的进程,即单线程进程。
二、Linux线程控制
2.1 POSIX线程库
pthread线程库是应用层的原生线程库:
- 应用层指的是这个线程库并不是系统接口直接提供的,而是由第三方帮我们提供的。
- 原生指的是大部分Linux系统都会默认带上该线程库。
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。
- 要使用这些函数库,要通过引入头文件<pthreaad.h>。
- 链接这些线程函数库时,要使用编译器命令的“-lpthread”选项。
错误检查:
-
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
-
pthreads函数出错时不会设置全局变量errno(而大部分POSIX函数会这样做),而是将错误代码通过返回值返回。
-
pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小。
2.2 线程创建
创建线程的函数叫做pthread_create
pthread_create函数的函数原型如下:
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,失败返回错误码。
让主线程创建一个新线程
当一个程序启动时,就有一个进程被操作系统创建,与此同时一个线程也立刻运行,这个线程就叫做主线程。
- 主线程是产生其他子线程的线程。
- 通常主线程必须最后完成某些执行操作,比如各种关闭动作。
下面我们让主线程调用pthread_create函数创建一个新线程,此后新线程就会跑去执行自己的新例程,而主线程则继续执行后续代码。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* startRoutine(void* args)
{while (true){cout << "线程正在运行..." << endl;sleep(1);}
}
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void*)"thread1");cout << "new thread id : " << tid << endl;//线程IDwhile (true){cout << "main thread 正在运行..." << endl;sleep(1);}return 0;
}
运行代码后可以看到,新线程每隔一秒执行一次打印操作,而主线程每隔两秒执行一次打印操作。
可以看到,主线程创建新线程后,二者一同运行着,且我们的新线程的ID很大。至于为什么这么大后续谈。
2.3 获取线程ID pthread_self
常见获取线程ID的方式有两种:
- 创建线程时通过输出型参数获得。
- 通过调用pthread_self函数获得。
pthread_self函数的函数原型如下:
pthread_t pthread_self(void);
调用pthread_self函数即可获得当前线程的ID,类似于调用getpid函数获取当前进程的ID。
- 例如,下面的代码,我们让主线程和新线程都通过pthread_self函数来获取自身的ID,并统一用16进制的方式打印。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
static void printTid(const char *name, const pthread_t &tid)
{printf("%s 正在运行, thread id: 0x%x\n", name, tid);
}
void* startRoutine(void* args)
{const char* name = static_cast<const char*>(args);while (true){printTid(name, pthread_self());sleep(1);}
}
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void*)"thread1");while (true){printTid("main thread", pthread_self());sleep(1);}return 0;
}
注意: 用pthread_self函数获得的线程ID与内核的LWP的值是不相等的,pthread_self函数获得的是用户级原生线程库的线程ID,而LWP是内核的轻量级进程ID,它们之间是一对一的关系。
2.4 线程等待pthread_join
首先需要明确的是,一个线程被创建出来,这个线程就如同进程一般,也是需要被等待的。如果主线程不对新线程进行等待,那么这个新线程的资源也是不会被回收的。所以线程需要被等待,如果不等待会产生类似于“僵尸进程”的问题,也就是内存泄漏。等待线程的函数叫做pthread_join,函数原型如下:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
参数说明:
- thread:被等待线程的ID。
- retval:线程退出时的退出码信息。
返回值说明:
- 线程等待成功返回0,失败返回错误码。
示例:
- 在下面的代码中我们先不关心线程的退出信息,直接将pthread_join函数的第二个参数设置为nullptr,等待线程后打印该线程的编号以及线程ID。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
static void printTid(const char *name, const pthread_t &tid)
{printf("%s 正在运行, thread id: 0x%x\n", name, tid);
}
void* startRoutine(void* args)
{const char* name = static_cast<const char*>(args);int cnt = 500;while (true){printTid(name, pthread_self());sleep(1);if (!(cnt--)) break;}cout << "线程退出啦...." << endl;return nullptr;
}
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void*)"thread1");sleep(1000);pthread_join(tid, nullptr);return 0;
}
如上我们发现,运行后,线程如约运行,并用ps axj命令查看此进程信息,当我们发送19号暂停此线程后,会发现我整个线程都跟着暂停了,当我发送18号信号,再次运行此线程时,又会发现线程又同时运行了,因为它们是在一个进程的。
下面更改代码,让新线程创建5s后退出,随后再过几秒后被thread_join等待,当主进程开始打印消息时,说明新线程join等待完成:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
static void printTid(const char *name, const pthread_t &tid)
{printf("%s 正在运行, thread id: 0x%x\n", name, tid);
}
void* startRoutine(void* args)
{const char* name = static_cast<const char*>(args);int cnt = 5;while (true){printTid(name, pthread_self());sleep(1);if (!(cnt--)) break;}cout << "线程退出啦...." << endl;return nullptr;
}
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void*)"thread1");sleep(10);pthread_join(tid, nullptr);cout << "main thread join success" << endl;sleep(10);while (true){printTid("main thread", pthread_self());sleep(1);}return 0;
}
我们使用如下监控脚本辅助我们观察现象:
while :; do ps -aL | head -1 && ps -aL | grep mytest; sleep 1; done
会发现当创建线程后,线程1正在运行,5s后新线程退出了,我们的监控脚本观察到线程由两个变成了一个,但是正常情况下预期应该是两个线程,随后线程等待成功,这里还是只能看到一个线程。不是说好退出后应该看到的是两个线程吗,事实上一个线程退出后我们并没有看到预期结果。原因是ps命令在查的时候退出的线程是不给你显示的,所以你只能看到一个线程。但是现在不能证明当前的新线程在退出没有被join的时候就没有内存泄漏。
- 所以线程退出的时候,一般必须要进行join,如果不进行join,就会造成类似于进程那样的内存泄漏问题。
来看下线程异常的问题:
- 如下的野指针问题:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
static void printTid(const char *name, const pthread_t &tid)
{printf("%s 正在运行, thread id: 0x%x\n", name, tid);
}
void *startRoutine(void *args)
{const char *name = static_cast<const char *>(args);int cnt = 5;while (true){printTid(name, pthread_self());sleep(1);if (!(cnt--)){int *p = nullptr;*p = 100; // 野指针问题}}cout << "线程退出啦...." << endl;return nullptr;
}
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread1");sleep(10);pthread_join(tid, nullptr);cout << "main thread join success" << endl;sleep(10);while (true){printTid("main thread", pthread_self());sleep(1);}return 0;
}
同样是使用如下的监控脚本辅助我们观察现象:
while :; do ps -aL | head -1 && ps -aL | grep mytest; sleep 1; done
此时会发现:待线程出现野指针问题时,左边会显示段错误,而右边监控脚本中的线程直接就没了。此时就说明当线程异常了,那么整个进程整体异常退出,线程异常 == 进程异常。所以线程会影响其它线程的运行 —— 线程的健壮性(鲁棒性)较低。
再来看看pthread_join等待函数的函数原型:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
问:如何理解第二个参数retval?
- 参数retval是线程退出时的退出码,这是一个二级指针,一个输出型参数。刚刚我们的代码中,以及涉及到了线程退出的方式(从线程函数return)。退出的类型是void*。
- ptherad_join的第二个参数retval的作用就是一个输出型参数,获取新线程退出时的退出码。我们先前讲过进程退出时,分为三种情况:
- 代码跑完,结果正确
- 代码跑完,结果不正确
- 异常
在线程退出时,代码跑完,结果不正确和结果正确都可以得到退出码,但是线程异常时并不会出现退出码。那么为什么异常时主线程没有获取新线程退出时的信号呢?
因为线程出异常就不再是线程的问题,而是进程的问题,应该让父进程获取退出码,知道它什么原因退出的。因此线程终止时,只需考虑正常终止.
2.5 线程终止
如果需要只终止某个线程而不是终止整个进程,可以有三种方法:
- 从线程函数return。
- 线程可以自己调用pthread_exit函数终止自己。
- 一个线程可以调用pthread_cancel函数终止同一进程中的另一个线程。
方法一(从线程函数return)
- 此法我们在上面已经见过,就不做演示。
方法二(pthread_exit)
- pthread_exit函数的功能就是终止线程,pthread_exit函数的函数原型如下:
#include <pthread.h>
void pthread_exit(void *retval);
参数说明:
- retval:线程退出时的退出码信息。
注意:
- 该函数无返回值,跟进程一样,线程结束的时候无法返回它的调用者(自身)。
- pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了。
例如,在下面代码中,我们使用pthread_exit函数终止线程,并将线程的退出码设置为1111:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
static void printTid(const char *name, const pthread_t &tid)
{printf("%s 正在运行, thread id: 0x%x\n", name, tid);
}
void *startRoutine(void *args)
{const char *name = static_cast<const char *>(args);int cnt = 5;while (true){printTid(name, pthread_self());sleep(1);if (!(cnt--)){break;}}cout << "线程退出啦...." << endl;//1、线程退出方式1: 从线程函数直接return/*return (void *)111;*///2、线程退出方式2: pthread_exitpthread_exit((void*)1111);
}
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread1");(void)n;void *ret = nullptr;pthread_join(tid, &ret);cout << "main thread join success, *ret: " << (long long)ret << endl;sleep(10);while (true){printTid("main thread", pthread_self());sleep(1);}return 0;
}
这段代码我们也能看出使用pthread_exit只能退出当前子线程,不会影响其它线程。
问:为何终止线程要用pthread_exit,exit不行吗?
看如下的代码:
#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
using namespace std;
__thread int global_value = 100;
void *startRoutine(void *args)
{while (true){cout << "thread" << pthread_self() << " global_value: " << global_value<< " Inc: " << global_value++ << "lwp: " << syscall(SYS_gettid) << endl;sleep(1);break;}exit(1);
}
int main()
{pthread_t td1;pthread_t td2;pthread_t td3;pthread_create(&td1, nullptr, startRoutine, (void *)"thread 1");pthread_create(&td2, nullptr, startRoutine, (void *)"thread 2");pthread_create(&td3, nullptr, startRoutine, (void *)"thread 3");int n = pthread_join(td1, nullptr);cout << n << ":" << strerror(n) << endl;n = pthread_join(td2, nullptr);cout << n << ":" << strerror(n) << endl;n = pthread_join(td3, nullptr);cout << n << ":" << strerror(n) << endl;return 0;
}
总结:
- exit是退出进程,任何一个线程调用exit,都表示整个进程退出。无论哪个子线程调用整个程序都将结束。 而pthread_exit的作用是只退出当前子线程。即使你放在主线程,它也会只退出主线程,其它线程有运行的仍会继续运行。
方法三(pthread_cancel)
- 线程是可以被取消的,我们可以使用pthread_cancel函数取消某一个线程,pthread_cancel函数的函数原型如下:
#include <pthread.h>
int pthread_cancel(pthread_t thread);
参数说明:
- thread:被取消线程的ID。
返回值说明:
- 线程取消成功返回0,失败返回错误码。
线程是可以取消自己的,取消成功的线程的退出码一般是-1。例如在下面的代码中,我们让线程执行一次打印操作后将自己取消:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
static void printTid(const char *name, const pthread_t &tid)
{printf("%s 正在运行, thread id: 0x%x\n", name, tid);
}
void *startRoutine(void *args)
{const char *name = static_cast<const char *>(args);int cnt = 5;while (true){printTid(name, pthread_self());sleep(1);if (!(cnt--)){// break;}}
}
int main()
{pthread_t td;int n = pthread_create(&td, nullptr, startRoutine, (void *)"thread1");(void)n;sleep(3);//代表main thread对应的工作cout << "new thread been canceled" << endl;pthread_cancel(td);void *ret = nullptr;pthread_join(td, &ret);cout << "main thread join success, *ret: " << (long long)ret << endl;sleep(10);while (true){printTid("main thread", pthread_self());sleep(1);}return 0;
}
为什么退出的结果是-1呢?
-
线程和进程一样,用的都是PCB,退出时都有自己的退出码,调用return或exit就是自己修改PCB中的退出结果(退出码),取消这个线程时,是OS取消的,就直接向退出码中写-1。
-
这里的-1就是pthread库里头给我们提供的宏(PTHREAD_CANCELED)
上述我们做的测试是让main thread主线程去取消新线程new thread,不推荐反过来。这里就不做测试了。
2.6 线程栈 && pthread_t
pthread_t实际上就是地址。
- 线程是一个独立的执行流
- 线程一定会在自己的运行过程中,产生临时数据(调用函数,定义局部变量等)
- 线程一定需要有自己的独立的栈结构
前面学习到,线程共享进程地址空间的内容,堆区也是的,堆区是动态申请的,线程内可以自己将其保持着,如果需要,这块空间是可以被其它线程保持可见性的,全局数据区也是如此:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int global_value = 100;
static void printTid(const char *name, const pthread_t &tid)
{printf("%s 正在运行, thread id: 0x%x, global_value: %d\n", name, tid, global_value);
}
void *startRoutine(void *args)
{const char *name = static_cast<const char *>(args);int cnt = 5;while (true){printTid(name, pthread_self());sleep(1);if (!(cnt--)){global_value = 200;}}cout << "线程退出啦..." << endl;int* p = new int(10);return (void*)p;
}
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread1");(void)n;while (true){printTid("main thread", pthread_self());sleep(1);}void *ret = nullptr;pthread_join(tid, &ret);cout << "main thread join success, *ret: " << *((int*)ret) << endl;delete (int*)ret;return 0;
}
如上我们设置全局变量为100,新线程和主线程在打印5次后,新线程对全局变量做修改,随后观察到的现象应该是全局变量由100变成200,主线程和新线程都应该是这个现象:
理解了数据区和堆区的划分,现在来看线程的独立栈结构。
我们使用的线程库,是用户级线程库:pthread。是因为Linux没有真线程,没有办法提供真的线程调用接口,只能提供创建子进程、共享地址空间的调用接口。但是进程的代码、数据……怎么划分这些都是由线程库自己维护的。注意:此pthread库是动态库。
当我们需要用到此动态库时,要把它加载到内存,随后映射进对应的进程地址空间。
因为要把此动态库加载到物理内存,所以我的磁盘中有如上(libpthread.so动态库 & mytest.exe可执行程序)。我们在运行时,首先要把此可执行程序mytest.exe加载到内存,此程序内部的代码中一定有pthread_create,pthread_join这些从libpthread.so动态库里调来的函数,所以此时OS把该动态库加载到内存。随后把此动态库经过页表映射到进程地址空间的共享区当中,我们的task_truct通过虚拟地址访问代码区然后跳转至共享区内,执行相关的创建线程等工作,执行后再返回至代码区。
- 所以最终都是在地址空间中的共享区内完成对应的线程创建等操作的。
- 所以在我们的代码中一定充斥着三大部分(你的,库的,系统的)。所有的代码都是在进程的地址空间当中进行执行的。
问:pthread_t究竟是什么呢?
既然我们已经知道此动态库会被加载到共享区,那么我们把此共享区的libpthread.so动态库放大来讨论。线程的全部实现,并没有全部体现在OS内,而是OS提供执行流,具体的线程结构由库来进行管理。如下:
操作系统只提供轻量级进程,对于用户他不管,只要线程。所以在用户和OS之间设计了libpthread.so库,用于创建线程,等待线程……操作。用户创建一个线程,库做了转换,让你在系统帮你创建一个轻量级进程,用户终止一个线程,库帮你终止一个轻量级进程,用户等待一个线程,库帮你转换成等待一个轻量级进程,并且把结果返回。此库起到的就是承上启下的作用。
库可以创建多个线程,需要对这些线程进行管理(先描述,再组织)。库里头通过类似struct thread_info的结构体(注意里头是有私有栈的)来进行管理:
struct thread_info
{pthread_t tid;void *stack; // 私有栈...
}
当你在用户层每创建一个线程时,在库里头就会创建一个线程控制块struct thread_info(描述线程的属性)。给创建线程的用户返回的是该结构体的起始虚拟地址。所以我们的pthread_t实际上就是用户级线程的控制结构体的起始地址!!!。
既然每一个线程都有struct thread_info结构体,而此结构体内部又有私有栈,所以结论如下:
- 主线程的独立栈结构,用的就是地址空间中的栈区
- 新线程用的栈结构,用的是库中提供的栈结构
2.7 线程的局部存储
我们的线程除了保存临时数据时可以有自己的线程栈,我们的pthread给我们了一种能力,如果定义了一个全局变量(默认所有线程共享),但是你想让每个线程各自私有,那么我们就可以使用线程局部存储。
- 如下我们创建了3个线程,创建一个全局变量,默认情况下此全局变量所有线程共享,现在我们来打印此全局变量以及地址来观察现象:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int global_value = 100;
void *startRoutine(void *args)
{while (true){cout << "thread" << pthread_self() << " global_value: " << global_value<< " &global_value: " << &global_value << " Inc: " << global_value++ << endl;sleep(1);}
}
int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 2");pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 3");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0;
}
正常情况下,我们观察到着三个线程打印的全局变量地址应该都是一样的,且打印的变量是在累加的,这是正常的,因为共享全局变量,我的修改别人也能拿到。
为了让此全局变量独属于各个线程所私有,我们只需要给全局变量前假设__thread即可,加了这个__thread就会默认把这个global_value再拷一份给每一个进程。
__thread int global_value = 100;
代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
__thread int global_value = 100;
void *startRoutine(void *args)
{while (true){cout << "thread" << pthread_self() << " global_value: " << global_value<< " &global_value: " << &global_value << " Inc: " << global_value++ << endl;sleep(1);}
}
int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 2");pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 3");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0;
}
如下可以看到,创建的3个线程,每个线程的全局变量的地址都是不一样的,修改变量时,互相之间没有影响,各自独立。
线程是有自己的轻量级进程lwp的,如果我们想要拿到此lwp,我们可以调用gettid函数获得
#include <sys/types.h>
pid_t gettid(void);
但是此函数不能直接使用,必须得调用syscall函数,在里头调用SYS_gettid才能拿到lwp。
#include <unistd.h>
#include <sys/syscall.h> /* For SYS_xxx definitions */
int syscall(int number, ...);
代码如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
using namespace std;
__thread int global_value = 100;
void *startRoutine(void *args)
{while (true){cout << "lwp: " << syscall(SYS_gettid) << endl;sleep(1);}
}
int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 2");pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 3");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0;
}
我们使用如下的监控脚本辅助我们观察现象:
ps -aL | head -1 && ps -aL | grep mytest
2.8 分离线程pthread_detach
-
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏。
-
但如果我们不关心线程的返回值,join也是一种负担,此时我们可以将该线程进行分离,后续当线程退出时就会自动释放线程资源。
-
一个线程如果被分离了,这个线程依旧要使用该进程的资源,依旧在该进程内运行,甚至这个线程崩溃了一定会影响其他线程,只不过这个线程退出时不再需要主线程去join了,当这个线程退出时系统会自动回收该线程所对应的资源。
-
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
-
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
分离线程的函数叫做pthread_detach,pthread_detach函数的函数原型如下:
#include <pthread.h>
int pthread_detach(pthread_t thread);
参数说明:
- thread:被分离线程的ID。
返回值说明:
- 线程分离成功返回0,失败返回错误码。
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
为什么我sleep(1)后才符合我们的预期呢?( 一个线程不能既是joinable又是分离的)。有sleep之后join就会失败,没有sleep,join就会成功,那么哪个才是正确的呢?
我们更倾向于让主线程去分离其它线程:
#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
using namespace std;
__thread int global_value = 100;
void *startRoutine(void *args)
{cout << "线程分离..." << endl;while (true){cout << "thread" << pthread_self() << " global_value: " << global_value<< " Inc: " << global_value++ << "lwp: " << syscall(SYS_gettid) << endl;sleep(1);}
}
int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 2");pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 3");//等待一秒为了防止,子线程还没分离,主线程就开始等待了。sleep(1);pthread_detach(tid1);pthread_detach(tid2);pthread_detach(tid3);//验证不能既分离又join等待int n = pthread_join(tid1, nullptr);cout << n << ":" << strerror(n) << endl;n = pthread_join(tid2, nullptr);cout << n << ":" << strerror(n) << endl;n = pthread_join(tid3, nullptr);cout << n << ":" << strerror(n) << endl;return 0;
}
总结分离线程:
- 线程分离了,意味着,不在关心这个线程的死活。所以这也相当于线程退出的第4种方式,延后退出。
- 立即分离或者延后分离都可以,但是要保证线程活着。
- 新线程分离,但是主线程先退出(进程退出),所有线程就都退了。
- 一般分离线程,对应的主线程不退出(常驻内存的进程)
三、线程的优缺点
3.1 线程的优点
-
创建一个新线程的代价要比创建一个新进程小得多。
-
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
-
线程占用的资源要比进程少很多。
-
能充分利用多处理器的可并行数量。
-
在等待慢速IO操作结束的同时,程序可执行其他的计算任务。
-
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
-
IO密集型应用,为了提高性能,将IO操作重叠,线程可以同时等待不同的IO操作。
概念说明:
- 计算密集型:执行流的大部分任务,主要以计算为主。比如加密解密、大数据查找等。
- IO密集型:执行流的大部分任务,主要以IO为主。比如刷磁盘、访问数据库、访问网络等。
3.2 线程的缺点
-
性能损失: 一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
-
健壮性降低: 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说,线程之间是缺乏保护的。
-
缺乏访问控制: 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
-
编程难度提高: 编写与调试一个多线程程序比单线程程序困难得多。
四、线程异常
- 单个线程如果出现除零、野指针等问题导致线程崩溃,进程也会随着崩溃。
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
五、线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率。
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。
相关文章:
Linux多线程详解
Linux多线程详解 一、Linux多线程概念1.1 什么是线程1.2 进程和线程1.3 进程的多个线程共享1.4 进程和线程的关系 二、Linux线程控制2.1 POSIX线程库2.2 线程创建2.3 获取线程ID pthread_self2.4 线程等待pthread_join2.5 线程终止2.6 线程栈 && pthread_t2.7 线程的局…...
攻防世界-web-1
Training-WWW-Robots 在URL后面加上/robots.txt 直接在URL后面添加/fl0g.php PHP2 他问我能不能登录这个网站,又因为考察php内容,在URL后面添加/index.php,无任何回显 试试/index.phps 分析一下代码,发现要用get方式上传idadmin,…...
笔记本+移动端维修全套教程
今天分享的是笔记本移动端维修全套教程(免费视频资料大全) 当自己手机或者电脑坏了,很多人都会想着去维修店铺修,但价格不透明,容易被坑,当自己了解一些之后,即使不会修,也可以对手…...
【STM32】知识点介绍二:GPIO引脚介绍
文章目录 一、概述二、GPIO的工作模式三、寄存器编程 一、概述 GPIO(英语:General-purpose input/output),即通用I/O(输入/输出)端口,是STM32可控制的引脚。STM32芯片的GPIO引脚与外部设备连接起来,可实现与外部通讯、…...
【STM32】GPIO
目录 1、什么是GPIO2、什么是GPIO组3、GPIO的基本结构4、GPIO位结构5、GPIO八种工作模式6、GPIO相关寄存器1. 端口配置低寄存器GPIO[x]_CRL和端口配置高寄存器GPIO[x]_CRH, Config Register High和Config Register Low)2. 端口输入数据寄存器(GPIO[x]_IDR)3. 端口输出数据寄存器…...
鸿蒙移动应用开发--UI组件布局
实验要求: 制作一个B站视频卡片界面,大致如下图所示,要求应用到线性布局、层叠布局等相关课堂知识。背景图、logo及文本内容不限。 实验环境 :DevEco Studio 实验过程: 步骤1:创建项目 1. 在您的开发环境…...
[MySQL]MySQL数据库基础知识与操作
MySQL基础知识 为什么要有数据库? 文件存储的缺点 1.没有以某种特定的数据格式存储数据,查找不方便,只能遍历2.安全性:数据误操作后不能回滚3.每次操作数据都要用户自己操作4.数据量大的时候,操作的成本很高 创建一…...
卡诺图化简法的原理
引子 若两个最小项只有一个因子不同,则称这两个最小项具有相邻性。 例如, A ′ B C ′ ABC A′BC′和 A B C ABC ABC两个最小项仅第一个因子不同,所以它们具有相邻性。这两个最小项相加时定能合并成一项并将一对不同的因子消去 A ′ B C ′…...
从零开始:使用Luatools工具高效烧录Air780EPM核心板项目的完整指南
本文将深入讲解如何使用Luatools工具烧录一个具体的项目到Air780EPM开发板中。如何使用官方推荐的Luatools工具(一款跨平台、命令行驱动的烧录利器),通过“环境配置→硬件连接→参数设置→一键烧录”四大步骤,帮助用户实现Air780E…...
探秘Transformer系列之(18)--- FlashAttention
探秘Transformer系列之(18)— FlashAttention 文章目录 0x00 概述0.1 问题0.2 其它解决方案0.3 Flash Attention 0x01 背景知识1.1 GPU相关概念硬件概念运行单元内存 软件概念运行模式线程模型Grid & DeviceBlock & SMThread & SPThread &am…...
VUE2导出el-table数据为excel并且按字段分多个sheet
首先在根目录下建一个文件夹export用来存储export.js import * as XLSX from xlsxfunction autoWidthFunc(ws, data) {// 设置每列的最大宽度const colWidth data.map(row > row.map(val > {var reg new RegExp([\\u4E00-\\u9FFF], g) // 检测字符串是否包含汉字if (v…...
Android面试总结之Android RecyclerView:从基础机制到缓存优化
引言 在 Android 开发中,RecyclerView是高效展示列表数据的核心组件。其强大的性能源于独特的视图复用机制和四级缓存体系。本文将结合源码与示例,带你深入理解RecyclerView的工作原理与优化策略。 核心组件 RecyclerView:作为容器视图&am…...
【C#语言】C#文件操作实战:动态路径处理与安全写入
文章目录 ⭐前言⭐一、场景痛点⭐二、完整实现代码⭐三、关键技术解析🌟1、动态路径处理🌟2、智能目录创建🌟3、安全的文件写入 ⭐四、进阶扩展方案🌟1、用户自定义路径选择🌟2、异常处理增强🌟3、异步写入…...
react中 useEffect和useLayoutEffect的区别
useEffect 和 useLayoutEffect 都是 React 中用于处理副作用的 Hook,但它们在执行时机和用途上有一些关键区别。理解这些区别可以帮助你更好地选择适合的 Hook 来实现特定的功能。 1. 执行时机 useEffect: 异步执行:useEffect 是在组件渲染完…...
TDengine 中的系统信息统计
简介 TDengine 3.0 版本开始提供一个内置数据库 performance_schema,Performance_Schema 数据库中存储了系统中的各种统计信息,包括存储及性能有关统计数据。本节详细介绍其中的表和表结构。 PERF_APP 提供接入集群的应用(客户端ÿ…...
C++可变参数
可变参数C风格的可变参数C风格可变参数的使用 C11可变参数模板递归展开参数包参数列表展开折叠表达式 STL中的emplace插入接口 可变参数 C风格的可变参数 可变参数是一种语言特性,可以在函数声明中使用省略号...来表示函数接受可变数量的参数。 例如典型的printf…...
建造者模式 (Builder Pattern)
建造者模式 (Builder Pattern) 是一种创建型设计模式,它将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。 一、基础 1.1 意图 将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。 1.2 适用场景 当创建复杂对象的算法应该…...
Thales靶机攻略
1.下载导入VBox,并启动靶机 靶机地址:https://download.vulnhub.com/thales/Thales.zip 解压后,在VBox中导入虚拟电脑。包含所有网卡的MAC地址。 导入完成,设置网卡模式为仅主机网络。开启靶机。 kali网卡更改为桥接模式。点击工…...
【redis】哨兵:搭建主从/哨兵节点详解和细节
文章目录 编排步骤搭建主从节点创建容器启动容器 搭建哨兵节点创建容器哨兵节点配置文件配置节点启动容器 主从/哨兵节点连入同一个局域网 编排步骤 分为两组 yml,先后启动 我们其实也可以用于一个 yml 文件,直接启动 6 个容器,但是&#x…...
零基础上手Python数据分析 (9):DataFrame 数据读取与写入 - 让数据自由穿梭
回顾一下,上篇博客我们学习了 Pandas 的核心数据结构 Series 和 DataFrame。 DataFrame 作为 Pandas 的 “王牌” 数据结构,是进行数据分析的基石。 但 DataFrame 的强大功能,还需要建立在 数据输入 (Input) 和 数据输出 (Output) 的基础上。 数据从哪里来? 分析结果又如何…...
Emacs 折腾日记(十九)——配置输入法和vim操作方式
上一篇文章中,我们将Emacs变得稍微好看了点。换成了自己喜欢的主题和颜色,这样每天用起来也比较养眼,不会特别排斥。本篇文章的主要任务就是配置输入法方便输入中文以及将vim的操作模式搬到Emacs中。进一步提到Emacs的可用性 配置中文输入法…...
Docker 镜像构建与优化
一、Dockerfile 构建镜像 1.1.拉取所需镜像 首先 docker pull 拉取一个 centos7 的镜像。 docker pull centos:7 下载 nginx 源码包。 官网:nginx: download wget https://nginx.org/download/nginx-1.26.3.tar.gz 1.2.解决 CentOS 7 安装源问题 因为原本的 …...
Mininet--moduledeps.py源码解析
整体构架概述 1. What is it moduledeps.py是Mininet网络模拟框架的模块依赖管理工具,用于动态管理Linux内核模块(如Open vSwitch、TUN/TAP)和验证系统环境。其核心目的是确保Mininet运行所需的底层模块和可执行文件已正确加载或存在&#…...
JAVA EE_多线程-初阶(一)
1.认识线程 1.1概念 1)线程是什么 线程是在进程内部中进行运行的,可以把它想成一个“执行流“,每个线程负责执行线程内的部分代码,多个线程之间可以”同时“执行多个代码。 “同时”:指并行,采用分时复用…...
批量优化与压缩 PPT,减少 PPT 文件的大小
我们经常能够看到有些 PPT 文档明明没有多少内容,但是却占用了很大的空间,存储和传输非常的不方便,这时候通常是因为我们插入了一些图片/字体等资源文件,这些都可能会导致我们的 PPT 文档变得非常的庞大,今天就给大家介…...
AI 的“幻觉”现象:深入解析 Hallucination 的成因与应对之道
文章目录 一、啥是 AI 的 Hallucination?二、啥时候容易出现幻觉?1. 知识边界之外的问题2. 模糊或不明确的输入3. 生成长篇内容4. 多模态任务中的误解5. 过度自信的语气要求 三、幻觉为啥会出现?原理是啥?1. 概率预测的本质2. 训练…...
加载huggingface数据集报token无效错误解决方案
加载huggingface数据集报错 import pandas as pddf pd.read_json("hf://datasets/udell-lab/NLP4LP/data/test.jsonl", linesTrue) print(df)PS C:\Users\pengkangzhen\PythonProjects\llm-ecr> & C:/Users/pengkangzhen/.conda/envs/py3.12_ml/python.exe …...
Java面试题及知识点Day1
自动拆箱和自动装箱 装箱就是自动将基本数据类型转换为包装器类型 拆箱就是自动将包装其类型转化为基本数据类型 重写和重载 重写 1.发生在子类和父类之间 2.参数的方法名,参数,返回值,必须相同 3.权限修饰符不能小于重写方法的权限修饰符…...
【动态规划】-- 三步问题(easy)
文章目录 1. 题目2. 题目解析3. 代码 1. 题目 在线oj 三步问题。有个小孩正在上楼梯,楼梯有 n 阶台阶,小孩一次可以上 1 阶、2 阶或 3 阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模 1000000007。…...
字符流Reader/Writer
一、Reader相关介绍及其子类 Reader是所有字符输入流的超类。它提供了读取字符流的基本方法,如read(), read(char[] cbuf, int off, int len)等;由于Reader是抽象类,通常使用它的子类如FileReader, BufferedReader等来创建字符输入流对象。 …...
字符串交替合并问题
问题: 给你两个字符串 word1 和 word2 。请你从 word1 开始,通过交替添加字母来合并字符串。如果一个字符串比另一个字符串长,就将多出来的字母追加到合并后字符串的末尾。 返回 合并后的字符串 。 示例 1: 输入:w…...
shell脚本一键安装docker+docker-compose,支持x86_64、arm64双架构
目录 脚本内容 运行效果 安装包和脚本 脚本内容 [roottest1 docker]# cat install.sh #!/bin/bash set -e export pathpwd export docker_data"/data/docker_data"function check_arch() {if [ -f /etc/redhat-release ]; thenOS"RedHat"elif [ -f /e…...
Elasticsearch 面试备战指南
Elasticsearch 面试备战指南 一、基础概念 什么是Elasticsearch? Elasticsearch是一个基于Lucene的分布式搜索和分析引擎,提供近实时搜索、高可用性和水平扩展能力。常用于日志分析(ELK)、全文检索、商业智能等场景。 Elasticsea…...
新手村:逻辑回归-理解04:熵是什么?
新手村:逻辑回归04:熵是什么? 熵是什么? 前置条件 在开始学习逻辑回归中的熵理论之前,需要掌握以下基础知识: 概率论与统计学: 概率分布(如伯努利分布、正态分布)。条件概率和贝叶斯定理。期…...
Javaweb后端登录会话技术jwt令牌
jwt生成与校验 是base4补位的 最后面是签名,签名不是base64,是通过签名算法加密后来的 令牌长度不是固定的,长度取决于原始内容,载荷,大小 头有,类型,签名算法 base64可以对任意的二进制数据进…...
图像对比分析并生成报告
pip install pyautogui """ 图像对比分析工具 功能:实现像素级差异、结构相似性(SSIM)、直方图相似度和特征匹配率四种对比方法 作者:智能助手 版本:1.2 日期:2025-02-27""" import os import cv2 …...
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加行拖拽排序功能示例1,TableView16_01.vue 基础行拖拽排序示例
前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加行拖拽排序功能示例1,TableView16_01.vu…...
vue-如何将组件内容作为图片生成-html2canvas
1.引入必要的库 这里呢我们使用 html2canvas 库来将 HTML 元素转换为画布(canvas),然后再将其导出为图片。首先,确保在项目中安装了 html2canvas: npm install html2canvas 2. 组件结构 然后在我们的vue文件里面&a…...
单片机 - RAM 与内存、ROM 与硬盘 之间的详细对比总结
RAM 与 内存 RAM(Random Access Memory,随机存取存储器) 和 内存 这两个术语通常是 同义词,即 内存 常常指的就是 RAM。 1. RAM(内存) 定义:RAM 是计算机中的 主存储器,用于临时存…...
Linux 练习一 NFS和DNS
练习四 任务需求:客户端通过访问 www.nihao.com 后,能够通过 dns 域名解析,访问到 nginx 服务中由 nfs 共享的首页文件,内容为:Very good, you have successfully set up the system. 各个主机能够实现时间同步&#…...
aab 转 apk
googleplay发布的游戏对外前,测试同学要安装到手机上先行测试,所以就有了这个需求。网上找了一篇文章讲的很详细了,文档是英语的,这里摘抄重要的部分做下记录: https://www.geekdashboard.com/extract-apk-files-from…...
JAVA开发:实例成员与静态成员
判断Java中的实例成员与静态成员 在Java中,可以通过以下几种方式判断一个成员是实例成员还是静态成员: 1. 通过声明方式判断 静态成员使用static关键字修饰,实例成员不使用: public class MyClass {// 实例成员int instanceVa…...
DeepSeek面试——模型架构和主要创新点
本文将介绍DeepSeek的模型架构多头潜在注意力(MLA)技术,混合专家(MoE)架构, 无辅助损失负载均衡技术,多Token 预测(MTP)策略。 一、模型架构 DeepSeek-R1的基本架构沿用…...
[项目]基于FreeRTOS的STM32四轴飞行器: 十二.角速度加速度滤波
基于FreeRTOS的STM32四轴飞行器: 十二.滤波 一.滤波介绍二.对角速度进行一阶低通滤波三.对加速度进行卡尔曼滤波 一.滤波介绍 模拟信号滤波: 最常用的滤波方法可以在信号和地之间并联一个电容,因为电容通交隔直,信号突变会给电容充电&#x…...
《基于SpringBoot的图书网购平台的设计与实现》开题报告
个人主页:大数据蟒行探索者 一、选题的依据及意义 1.1选题来源 市场需求驱动:如今,互联网深度融入人们生活,阅读场景愈发多元化,线上购书成为主流趋势之一。读者期望随时随地浏览海量图书资源,对比价格…...
Python 非异步函数执行异步函数的方案
import asyncio import sys from your_module import browser_main # 替换为你的实际模块名 async def _keep_alive(): """保持程序持续运行""" while True: await asyncio.sleep(1) def run_browser(index: int None): """ 执…...
计算机期刊推荐 | 工程技术-电子与电气, 计算机
IET Microwaves Antennas & Propagation的新特刊:《天线与超材料的设计、合成、仿真与实验》 学科领域: 工程技术-电子与电气, 计算机 期刊类型: SCI/SSCI/AHCI 收录数据库: SCI(SCIE) ISSN: 1751-8725 中科院…...
【开源宝藏】30天学会CSS - DAY9 第九课 牛顿摆动量守恒动画
以下是一份逐步拆解教程,带你从零理解并复刻这个牛顿摆(Pendulum of Newton)动画效果,这是一个经典的物理演示模型,现在通过纯 HTML 和 CSS 实现出来,视觉效果炫酷、结构简洁。 🎯 动画效果说明…...
对三维物体模型的阈值操作
对三维物体模型的阈值操作 1. 使用point_coord_x、point_coord_y、point_coord_z阈值分割麻辣兔头2. point_normal_x、point_normal_y、point_normal_z有什么区别?3. 去除离群点 1. 使用point_coord_x、point_coord_y、point_coord_z阈值分割麻辣兔头 dev_open_win…...
【Unity网络编程知识】使用Socket实现简单TCP通讯
1、Socket的常用属性和方法 创建Socket TCP流套接字 Socket socketTcp new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 1.1 常用属性 1)套接字的连接状态 socketTcp.Connected 2)获取套接字的类型 socketTcp.So…...