文章目录
- 📖 前言
- 1. 线程的id
- 1.1 pthread_self:
- 1.2 线程独立栈结构:
- 1.3 pthread_t究竟是什么:
- 1.4 线程的局部存储:
- 2. 线程退出的三种方式
- 2.2 - 1 方式一:pthread_cancel
- 2.2 - 2 方式二:pthread_exit
- 2.2 - 3 方式三:隐式退出
- 3. 线程的分离
- 3.1 新线程分离后,主线程先退出:
- 3.2 立即分离,但是还是join到了:
- 3.3 线程退出的第四种方式:
📖 前言
上一章我们认识了什么是线程,对线程有个基本的概念,了解并学习了进程和线程的关系,对页表有了更深层次的理解,还对查看进程有了了解。
本章我们再继续学习线程,将对线程id进行了解,线程私有栈等,接下来学习线程控制,线程退出,线程分离等,目标确定,办好小板凳准备开讲了……
1. 线程的id
- 线程创建和进程一样也得有线程id。其实十六进制打印出来的
pthread_t tid
,是个地址。
1.1 pthread_self:
谁调用这个函数,就把线程id返回:
返回值:
- 在多线程程序中,如果一个变量定义在全局作用域内,则它是一个全局变量。
- 全局变量具有全局可见性,可以被程序中的任何线程访问。
- 因为全局变量存储在进程的数据段中,所有线程都可以访问这个数据段。
1.2 线程独立栈结构:
- 线程是一个独立的执行流。
- 线程一定会在自己的运行过程中,产生临时数据(调用函数,定义局部变量等)。
- 所以线程一定需要有自己的独立的栈结构。
- 代码区好划分,以函数的方式让线程各自执行一份。
- 数据不需要划分,数据属于全局,所有线程可以共享。
- 堆区每个线程可以自己去申请,虽然一个线程申请的堆区堆其他线程也是保持可见性的,但是通过保留地址(定义全局指针),可以对其他线程保持可见性。
像我们调用的pthread_create,pthread_join
这些函数都不是我们自己实现的,但是我们可以使用它,是因为我们链接了线程库。
- Linux的线程库,虽然是原生的,在系统当中会内置的线程库,但是依旧是用户级线程库。
- 因为Linux没有真线程,就没办法提供真线程的调用接口。
共享库(Shared Library)
是被所有线程共享的。
用了动态库就要将动态库加载到内存里,映射到地址空间当中:- 我们使用的线程库,虚拟地址访问代码区,访问对应接口时
- 可以在地址空间内进行跳转,跳转到共享区
- 然后把对应的相关的创建线程的一大堆工作全部做完
- 做完后把结果返回,此时线程也就被创建好了
所有的代码执行,都是在进程的地址空间当中进行执行的。
1.3 pthread_t究竟是什么:
libpthread. so
是Linux系统中用于支持多线程编程的共享库,其中包含了与线程管理相关的函数和数据结构。
Linux有轻量级进程,能够模拟实现线程,但是就只要靠内核当中的轻量级进程就能把线程的所有工作全做了吗?如果全部都做了,那么为什么不提供系统接口呢?
—— 其实并没有全部都做了。
- 线程的全部实现,并没有全部体现在OS内,而是OS提供执行流,具体的线程结构由库来进行管理。
库可以创建多个线程,那么库要不要管理线程呢?答案是肯定的。
- 要想管理就要,先描述,再组织。
- 再库里面有一个
struct thread_info
结构体,是用来描述创建的线程的。 - 这个结构体中有线程的
tid
,还有一个重要的就是指向线程私有栈的栈顶指针。
目前地址空间内只看到了一个栈。
pthread_t
对应的就是用户级线程的控制结构体的起始地址:
- 可以通过该地址找到线程相关属性。
- 给创建线程的用户返回的是,线程控制结构体的起始地址。
- 用户的所有创建线程、等待线程、获取线程的各种属性,都是这个库帮我们去做的,控制进程内的轻量级进程来完成的。
- 每一个线程都有一个
thread_info
,而它的属性里面就有私有栈。 - 所以最终的结论就是,主线程的独立栈结构,用的就是地址空间中的栈区,新线程用的栈结构,用的是库中提供的栈结构。
- 在Linux内核中,每个进程都有一个相应的
task struct
结构体来描述进程的信息,而每个线程都是作为进程的一部分存在的,所以也有对应的task_struct
结构体来描述线程的信息。- 在
task_struct
结构体中,有一个指向线程特定数据(Thread-Specific Data,TSD)
的指针字段thread_info
。thread_info
是一个指向thread_info结构体的指针,它包含了很多与线程执行相关的信息,其中包括线程的栈顶指针
(Stack Pointer)`。thread_info
结构体中的栈顶指针字段通常称为sp
或rsp
,具体名称可能会因架构和内核版本而有所不同。通过访问thread_info
结构体中的栈顶指针字段,可以获取当前线程的栈顶位置。struct thread_info
结构体存储在内核空间中,只能由操作系统内核访问和管理。它是内核用来追踪和管理线程状态的重要数据结构,用户空间的应用程序无法直接访问或修改它。- 总而言之,在Linux内核中, 每个线程的私有栈的栈顶指针通常是存储在对应线程的
thread_info
结构体中的一个字段中,可以通过该指针来获取线程栈的栈顶位置。
pthread_t
到底是什么类型?取决于实现。
pthread_t
类型的线程ID,本就是一个进程地址空间上的一个地址。
线程库在底层与内核进行交互,通过系统调用接口(如 clone)来创建和管理内核线程:
- 每创建一个线程,库里面可不仅仅有代码,库里面也有数据。
- 当用户层调用创建一个线程时,在库里面会为我们创建一个我们所对应的
thread_info
这样的结构线程控制块。 - 里面有个指针指向用户空间的某个位置,代表曾经申请的栈空间,这个空间是怎么设置的,我们不管。
- 实际上这个空间也是由用户传的,底层调用的是clone。
用户级线程:栈是由库来维护,但是还是用户提供的。
备注:
pthread_info
不是在库里面直接创建的,而是由操作系统内核在创建线程时生成的线程控制块的信息。- 可以通过一些系统调用或库函数来获取线程的相关信息,如线程ID、优先级等。
- 具体的获取方式可能因操作系统和编程语言的不同而有所差异。
- 线程是在库当中维护的,对应的执行流是由操作系统去维护,对应的线程相关的用户需要的一些属性是由库里面维护的,线程对应的私有栈是每一个线程控制结构保存的。
- 线程对应的
pthread_t id
是线程控制结构的起始地址。
最终结论:
- 主线程的独立栈结构,用的就是地址空间中的栈区,新线程用的栈结构,用的是库中提供的栈结构。
- Linux中,线程库用户级线程库,和内核的LWP是
1 : 1
。 - 一个用户级线程,一个内核的LWP。
pthread_ t
本质是个地址,是pthread
原生线程库被load到内存,并映射进当前进程的地址空间之后,地址空间里在线程库内会为我们当前线程创建对应线程的相关信息。
比如说,线程的描述结构体,线程的局部存储,线程的独立栈,主线程用的依旧是地址空间的栈区,而新线程用的则是共享区当中,以及在用户空间当中给我们提供的对应的栈区。
一个线程要是出异常,整个进程都挂掉了。
也可以在线程内部malloc
空间,也可以将对应的结果数据返回,新线程和主线程之间,两个线程之间要进行值的交换是可以通过堆区进行交互的,不定非要硬传参数。
主线程创建的对象,在新线程依旧能看到,这说明,在多个线程里面地址空间内,堆空间也是有可见性的。
1.4 线程的局部存储:
- 线程的局部存储,代表的是,我们的线程除了保存临时数据时有自己的线程栈,我们的
ptread库
还提供了一种能力: -
- 如果我们定义了一个全局的变量,但是这个全局的变量想让每个线程各自私有,那么就可以使用线程局部存储这样的概念。
-
- 在全局变量前加上
__thread
即可。
- 在全局变量前加上
-
- 可以理解成将这个全局变量都拷贝一份给对应的线程,所以每个线程都有自己的。
2. 线程退出的三种方式
2.2 - 1 方式一:pthread_cancel
代码演示:
#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){// cout << "线程正在运行..." << endl;printTid(name, pthread_self());sleep(1);// if (cnt-- == 0) break;// if (cnt-- == 0)// {// int* p = nullptr;// *p = 100; // 野指针问题// }}// 新线程运行5s后退出cout << "线程退出啦……" << endl;// 1. 线程退出的方式: return// 通过pthread_join获得这个函数的返回值// 返回值是个void*,所以为了拿到这个void*需要传进去void**// 一个输出型参数// 2. 线程退出方式,pthread_exitpthread_exit((void*)111);// return (void*)10;
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void*)"thread1");// 代表main thread对应的工作,一创建就取消sleep(3);// 3. 线程退出方式,给线程发送取消请求,如果线程是被取消的,退出结果是: -1pthread_cancel(tid);cout << "new thread been canceled" << endl;// cout << "new thread id: " << tid << endl; // 线程ID -- 为什么这么大?// PTHREAD_CANCELED;// 主线程运行10s后退// sleep(10);// 线程退出的时候,一般必须要进行join,如果不进行join// 就会造成类似于进程那样的内存泄漏的问题(没有僵尸线程这样的说法)// 线程对应的退出结果暂时不获取void* ret = nullptr;pthread_join(tid, &ret); // void** retval是一个输出型参数cout << "main thread join success, *ret: " << (long long)ret << endl;// sleep(10);while (true){cout << "main thread 正在运行..." << endl;printTid("main thread", pthread_self());sleep(1);}return 0;
}
- 新线程被创建出来就如5s后就会退,退出之后,看到的线程对应的信息应该是还有的。
- 因为还没有被join,5s后被join后当我们主线程开始打印消息时,说明join完成。
- 我们看到的现象是线程退出时,查看时却少了一个,预期应该是两个线程,应该等到join之后才变成一个线程。
- ps命令在查的时候,那种退出的线程就不显示了。
取消一个线程:
返回值:
- 我们上述代码是主线程取消新线程,但是如果要是反过来呢,用新线程取消主线程可以吗?答案是可以的,但是不推荐这种做法。
-
- 如果新线程取消了主线程(调用了pthread_cancel函数取消主线程),主线程会立即停止执行,并开始清理资源。
-
- 取消主线程可能导致未完成的操作无法完整执行,因此需要在编写线程代码时小心处理取消请求。
-
- 我这边自己的验证是,主线程退出了,但是剩余的线程依旧在执行。
正常退出:
取消后退出:
2.2 - 2 方式二:pthread_exit
退出线程:
pthread_exit
是一个线程函数的库函数,可以让线程提前退出执行并返回一个指定的退出码。- 调用
pthread_exit
函数会立即终止当前线程的执行,而不管该线程是否已经完成其任务。 - 因此,在调用
pthread_exit
函数之前,需要确保所有资源已经正确地释放。
当调用pthread_join函数等待一个线程结束时:
- 如果被等待的线程通过
pthread_exit
函数退出,则该线程的返回值会被传递给pthread_join
函数的第二个参数所指向的变量。
如果使用exit(1),那么整个进程就退出了。
2.2 - 3 方式三:隐式退出
当线程函数执行完毕并返回时,线程会隐式地退出。在这种情况下,线程的退出码默认为0,表示正常终止。
对于线程的隐式退出,即通过线程函数显式返回一个值(使用return语句),是可以被pthread_join
获取到的。当线程通过显式返回方式终止时,其返回值会被传递给pthread_join
函数。
3. 线程的分离
- 默认情况下,新创建的线程是
joinable
的,线程退出后,需要对其进行pthread_join
操作,否则无法释放资源,从而造成系统泄漏。 - 如果不关心线程的返回值,
join
是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
一个线程要是被分离了,就不能被join了。分离和join是矛盾的。新线程不退出,主线程就卡在那里,等待新线程join。
joinable是可被等待的状态。
#include <iostream>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <sys/syscall.h>
#include <unistd.h>using namespace std;// 只要是带了__thread,可以理解成将这个全局变量都拷贝一份给对应的线程,所以每个线程都有自己的
int global_value = 100;void* startRoutine(void* args)
{// 线程分离:(自己分离自己)// pthread_detach(pthread_self());// cout << "线程分离了....." << endl;while (true){cout << "thread " << pthread_self() << " global_value: "<< global_value << " &global_value: " << &global_value << " Inc: " << global_value++ << " lwp " << syscall(SYS_gettid) << endl;sleep(1);}// 退出进程,任何一个线程调用exit,都表示整个进程退出// exit(1);// pthread_exit()
}int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(&tid1, nullptr, startRoutine, (void*)"thread1");pthread_create(&tid2, nullptr, startRoutine, (void*)"thread2");pthread_create(&tid3, nullptr, startRoutine, (void*)"thread3");// sleep之后才能看到join失败的情况// sleep是为了这三个线程都detach之后再进行分离sleep(1);// 因为主线程和新线程谁先被调度是不一定的,有可能,join先被阻塞挂起了,而新线程还没有detach// 更倾向于:让主线程,分离其他线程pthread_detach(tid1);pthread_detach(tid2);pthread_detach(tid3);int n = pthread_join(tid1, nullptr);cout << n << ":" << strerror(n) << endl;pthread_join(tid2, nullptr);cout << n << ":" << strerror(n) << endl;pthread_join(tid3, nullptr);cout << n << ":" << strerror(n) << endl;return 0;
}
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
线程分离的情况:
- 立即分离(压根不关系线程执行的怎么样)。
- 延后分离(线程跑一段时间后再分离)。
- 新线程分离,但是主线程先退出(进程退出)。一般我们分离线程时,对应的主线程一般不要退出(常驻内存的进程)。
- 一旦主线程退出了,所有新线程也会跟着释放,主线程退出代表进程退出,地址空间页表、代码、数据等都会释放。
- 新线程退出了则不会影响进程的继续运行。
- 线程分离了,线程退出就会自动被系统回收。
- 这就意味着,我们不再关心这个线程的死活。
3.1 新线程分离后,主线程先退出:
将新线程全部都分离,然后主线程中也不去join新线程了,主线程执行完,直接退出:
运行结果:
我们发现:三个新线程一并退出了。
进程退出的时候,操作系统就回收了这个进程的程序地址空间,连资源都被释放了,线程就没有办法继续运行,自然就退出了。
为了避免这种现象,一般我们分离线程的时候,都倾向于让主线程保持在后台运行(常驻内存的程序)。
3.2 立即分离,但是还是join到了:
void *startRoutine(void *args)
{// 线程分离:(自己分离自己)pthread_detach(pthread_self());cout << "线程分离了....." << endl;while (true){cout << "thread " << pthread_self() << " global_value: "<< global_value << " &global_value: " << &global_value << " Inc: " << global_value++<< " lwp " << syscall(SYS_gettid) << endl;sleep(1);}// 退出进程,任何一个线程调用exit,都表示整个进程退出// exit(1);// pthread_exit()
}
- 这是因为,我们创建一个线程的时候,它会先去执行线程创建的相关代码。
- 此时主线程,又直接去执行后面的代码了。
- 此时
pthread_join
的调用是成功的,因为线程自己的detach
代码还没有被执行!
此时只需要将主线程在分离之后等等,再去join。
3.3 线程退出的第四种方式:
线程分离之后,延后退出。