上一章我们认识了什么是线程,对线程有个基本的概念,了解并学习了进程和线程的关系,对页表有了更深层次的理解,还对查看进程有了了解。
本章我们再继续学习线程,将对线程id进行了解,线程私有栈等,接下来学习线程控制,线程退出,线程分离等,目标确定,办好小板凳准备开讲了……
pthread_t tid
,是个地址。谁调用这个函数,就把线程id返回:
像我们调用的pthread_create,pthread_join
这些函数都不是我们自己实现的,但是我们可以使用它,是因为我们链接了线程库。
共享库(Shared Library)
是被所有线程共享的。所有的代码执行,都是在进程的地址空间当中进行执行的。
libpthread. so
是Linux系统中用于支持多线程编程的共享库,其中包含了与线程管理相关的函数和数据结构。
Linux有轻量级进程,能够模拟实现线程,但是就只要靠内核当中的轻量级进程就能把线程的所有工作全做了吗?如果全部都做了,那么为什么不提供系统接口呢?
—— 其实并没有全部都做了。
库可以创建多个线程,那么库要不要管理线程呢?答案是肯定的。
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
这样的结构线程控制块。用户级线程:栈是由库来维护,但是还是用户提供的。
pthread_info
不是在库里面直接创建的,而是由操作系统内核在创建线程时生成的线程控制块的信息。pthread_t id
是线程控制结构的起始地址。最终结论:
1 : 1
。
pthread_ t
本质是个地址,是pthread
原生线程库被load到内存,并映射进当前进程的地址空间之后,地址空间里在线程库内会为我们当前线程创建对应线程的相关信息。
比如说,线程的描述结构体,线程的局部存储,线程的独立栈,主线程用的依旧是地址空间的栈区,而新线程用的则是共享区当中,以及在用户空间当中给我们提供的对应的栈区。
一个线程要是出异常,整个进程都挂掉了。
也可以在线程内部malloc
空间,也可以将对应的结果数据返回,新线程和主线程之间,两个线程之间要进行值的交换是可以通过堆区进行交互的,不定非要硬传参数。
主线程创建的对象,在新线程依旧能看到,这说明,在多个线程里面地址空间内,堆空间也是有可见性的。
ptread库
还提供了一种能力:__thread
即可。代码演示:
#include
#include
#include
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_exit
pthread_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. 线程退出方式,给线程发送取消请求,如果线程是被取消的,退出结果是: -1
pthread_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;
}
取消一个线程:
正常退出:
退出线程:
pthread_exit
是一个线程函数的库函数,可以让线程提前退出执行并返回一个指定的退出码。pthread_exit
函数会立即终止当前线程的执行,而不管该线程是否已经完成其任务。pthread_exit
函数之前,需要确保所有资源已经正确地释放。当调用pthread_join函数等待一个线程结束时:
pthread_exit
函数退出,则该线程的返回值会被传递给pthread_join
函数的第二个参数所指向的变量。如果使用exit(1),那么整个进程就退出了。
当线程函数执行完毕并返回时,线程会隐式地退出。在这种情况下,线程的退出码默认为0,表示正常终止。
对于线程的隐式退出,即通过线程函数显式返回一个值(使用return语句),是可以被pthread_join
获取到的。当线程通过显式返回方式终止时,其返回值会被传递给pthread_join
函数。
joinable
的,线程退出后,需要对其进行pthread_join
操作,否则无法释放资源,从而造成系统泄漏。join
是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。一个线程要是被分离了,就不能被join了。分离和join是矛盾的。新线程不退出,主线程就卡在那里,等待新线程join。
joinable是可被等待的状态。
#include
#include
#include
#include
#include
#include
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;
}
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
线程分离的情况:
将新线程全部都分离,然后主线程中也不去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。
线程分离之后,延后退出。