文章目录
- 线程的id和LWP
- 线程的终止
- 线程的返回值问题
- 关于原生线程库问题
本篇总结的内容主要是关于线程的控制专题
线程的id和LWP
对于获取线程的id来说,在Linux系统中存在这样的调用
这个调用就可以获取返回当前线程的id
先写出下面的实例代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
// 新线程
void *ThreadRoutine(void *arg)
{
const char *threadname = (const char *)arg;
while (true)
{
cout << threadname << " thread: "
<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread-1");
while (true)
{
cout << "main thread: "
<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;
sleep(1);
}
return 0;
}
从这个代码中可以看出一个现象,那就是获取的线程的id和LWP是不一样的,这也就意味着LWP和id是两个不同的内容,如果将这个数字转换成十六进制,会发现这个数字其实很像一个地址,而事实上这个东西确确实实是一个地址,关于这个地址的概念在后续会进行阐述
线程的终止
那么下面要进行讨论的内容是,线程的终止问题,对于一个线程来说,它如何进行终止?由于线程在Linux中可以看成是一个轻量级的进程,那么这里先回顾一下对于进程的终止概念,对于进程来说想要终止可以进程退出,或是通过return进行退出,那么在这里也可以类比过来,线程也可以进行适当的退出操作,但是在之前的内容中知道,线程是不可以直接进行exit的,线程直接进行exit会导致所有的线程都进行退出,原因就在于exit表示的是进程退出,而不是线程退出,所以任何一个线程调用这个exit函数都会导致整个所有的线程都退出,因为它们共同属于一个进程
那么线程想要单独退出也是有对应的退出接口的,其中一个退出接口就叫做pthread_exit调用
那么这个接口的作用就是直接对应的线程进行终止掉,所以现在想要终止一个线程,不仅可以调用return语句进行调用,也可以使用对应的调用接口进行退出
线程的返回值问题
下面来到的话题是关于线程的返回值问题,那对于线程的返回值问题来说,首先要搞清楚的是返回值的意义是什么,那这个问题其实很好明白,就是为了让外部的主线程能够获取到对应的返回值,进而进行对应的操作,那下面的问题是如何获取?如果没有及时获取是否会出现类似于进程的僵尸问题?
获取线程的返回值
获取线程的返回值,在Linux系统中存在一个系统调用:
这个系统调用的作用之一,就是可以获取到指定线程的退出信息,第二个参数是一个二级指针,表明这是一个输出型参数,它想要获取的内容是一个void*类型的参数,而这个参数实际上就是对应在创建线程的时候调用时会返回的值,这个函数可以回收指定线程id的线程,并且把返回值的信息存储到第二个这个输出型参数当中,这样就能完成对于线程的返回值获取
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
class pthread_exit_info
{
public:
pthread_exit_info(int code, const string &info)
: _code(code), _info(info)
{
}
public:
int _code;
string _info;
};
// 新线程
void *ThreadRoutine(void *arg)
{
const char *threadname = (const char *)arg;
cout << threadname << " thread: "
<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;
cout << threadname << " thread exit" << endl;
sleep(2);
return (void *)new pthread_exit_info(0, "exit normal");
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread-1");
cout << "main thread: "
<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;
sleep(5);
void* res = nullptr;
pthread_join(tid, &res);
pthread_exit_info* exit_info = (pthread_exit_info*)res;
cout << "线程的退出码是: " << exit_info->_code << " 线程的退出信息是: " << exit_info->_info << endl;
return 0;
}
线程的僵尸问题
对于进程来说,如果子进程结束了,但是父进程一直没有对子进程进行资源的回收,那么子进程就会进入僵尸状态,子进程的PCB等资源还在内存中进行占用,需要父进程调用对应的接口进行回收,那对于线程来说当然也会存在这样的问题,但是这个现象不是很明显的观察,所以这里只是会输出这样的观点,确实在操作系统内部会存在线程的僵尸状态的问题,但没有一个明确的规定标准
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
// 新线程
void *ThreadRoutine(void *arg)
{
const char *threadname = (const char *)arg;
cout << threadname << " thread: "
<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;
cout << threadname << " thread exit" << endl;
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread-1");
cout << "main thread: "
<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;
sleep(5);
pthread_join(tid, nullptr);
return 0;
}
从上述的现象图中确实可以看到,是存在一个这样的退出的动态过程的,而这个join的过程也是一个阻塞等待的过程,所以在观察现象的时候可以看到,是存在这样的一个从2个线程创建出来到1个线程到没有线程这样的一个过程
补充:
1. 进程等待回收资源的时候,会有一个参数用来获取子进程的退出时的退出信息,包括有信号等等内容,那线程为什么没有?
这个问题其实不难理解,进程退出会有对应的信息是因为父进程不知道子进程的退出情况是什么,可能是正常退出也可能是异常退出,但是在今天看来,主线程并不关心这个问题,原因在于如果新建的进程出现了异常,那么直接整个进程都退出了,所以只要这个线程返回,那么这个线程必然是正常进行退出的,因此从这个角度来讲,确实不需要用来存储异常的信息
2. 如果子线程一直不退出,主线程要一直在这里等待吗?
答案确实是如此,因为这个调用是一个阻塞形式的等待,所以主线程必须卡在这里等这个新线程的返回信息,默认情况下这是阻塞等待,当然也有特殊情况,比如可以对这个新线程设置一个分离状态,表示主线程此时不关心新线程的退出情况了,可以退也可以不退,这个就叫做分离状态
那如何进行线程的分离?
方法也很简单,只需要调用对应的系统调用即可:
3. 线程的取消
线程的取消如何理解?简单来说就是当线程正在运行的时候把它进行取消,下面用代码来实现:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
class pthread_exit_info
{
public:
pthread_exit_info(int code, const string &info)
: _code(code), _info(info)
{
}
public:
int _code;
string _info;
};
// 新线程
void *ThreadRoutine(void *arg)
{
const char *threadname = (const char *)arg;
while (true)
{
cout << threadname << " thread: "
<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;
sleep(1);
}
cout << threadname << " thread exit" << endl;
return (void *)new pthread_exit_info(0, "exit normal");
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread-1");
cout << "main thread: "
<< ", pid: " << getpid() << ", tid: " << pthread_self() << endl;
sleep(2);
cout << "线程取消" << endl;
int n = pthread_cancel(tid);
void *ret = nullptr;
pthread_join(tid, &ret);
std::cout << "main thread join done,"
<< " n: " << n << " thread return: " << (int64_t)ret << std::endl;
return 0;
}
那在线程被取消后,线程取消函数会返回一个0,而获取线程的退出信息会获取到一个-1,这个-1代表的是一个宏,表示这个线程是因为被线程取消了而退出的:
基于上述的内容可以看出,线程是可以被取消的,线程是一个死循环,但是依旧不影响它可以被取消,如果这个被取消的线程处于分离状态,那么这个线程被取消后会被操作系统自动回收掉对应的资源
关于原生线程库问题
结束了上面的话题,那么下面一个话题是,关于Linux的原生线程库的问题
在Linux操作系统中,操作系统内部的线程不是真线程,而是用进程模拟的线程,所以Linux系统中不会直接提供对应的任何关于创建线程的系统调用,最多是提供创建轻量级进程的系统调用,但是对于正常的操作系统来说,操作系统是有必要提供对应的线程的,那如何处理这个问题呢?所以就在Linux的系统和用户之间建立起来了一个软件层,这个软件层的作用之一就是负责在底层调用为系统创建对应的轻量级进程的接口,其次在上层返回的时候是一个操作线程的接口,比如说有创建线程,终止线程等等
所以在Linux系统中关于线程的一个解决方案是,使用了一个线程的库来解决的,这个原生线程库的意思就是系统会自带有这个库,如果没有这个库就不能正常运行和线程相关的进程
站在用户层的角度来讲,在用户创建了5个线程后,那么在系统中就会创建5个轻量级进程与之对应,所以在进行查询的时候就能找到5个对应的LWP,对于轻量级进程的管理操作系统自己就可以做到,所以直接复用之前的代码就可以了,但是对于用户来讲却并不是这样,用户想知道现在创建了多少个线程,每一个线程的状态是什么,当前有多少个,某个线程的退出信息是什么,这些该如何获取?从刚才的代码中可以看出可以通过join函数来获取,但是join函数的底层又该从哪里获取这些信息呢?
换句话说,Linux的线程是叫做用户级线程,从字面意思可以看出,这个线程是用户层上的概念,而在内核的角度看只有只有轻量级进程的概念,那操作系统是可以有这个能力把轻量级进程管理起来的,这是操作系统的本职工作,但是对于系统的原生库来说,它又该如何获取信息呢?它该如何进行管理信息呢?
结论是,当操作系统中如果存在了大量的轻量级进程,那么注定对应的原生库也会有自己的方法来进行管理,所以就引出了一个叫做tcb的概念,tcb是对标PCB的一个概念,可以理解成是一个线程描述符的概念,在这个tcb中必然会存在很多的信息,其中就包括有LWP这样的概念,因此换句话说可以理解成,在这样的库中就会存在一些封装的概念,同时还会为每一个线程封装一个简单的tcb,当然这个概念在Linux中是没有的
因此会引入下图这样的概念,类似于文件库,文件库中是可以申请有一段空间作为文件缓冲区,所以线程其实也有这样的能力,而下图中的线程栈就是这样的概念:
主线程的栈通常是在程序启动时由操作系统分配的真实的栈空间,它是在程序的虚拟地址空间中预留的一部分。这个栈空间是在程序的内存映像中分配的,因此可以被认为是在真实的栈上。
而对于其他创建的新线程,它们的栈空间可能由堆空间代替而形成,意味着它们的栈空间是在堆上动态分配的。这种动态分配使得线程栈的大小可以根据需要进行调整,并且可以更灵活地管理内存。
库是要被加载到共享区的,加载到共享区之后就会在这块区域内维护一个区域,这个区域其实就会分配一段空间,然后把这段空间的地址放到这个库当中,那么在线程进行运行的时候就可以在库中的指针找到这个线程所对应的地址空间了,换句话说,如果未来想要把这个线程进行退出,实际上就是把线程终止,把轻量级进程终止,把线程所维护的这段空间释放掉,就完成了释放的工作
那如果此时创建了很多线程呢?这些线程都会在这个库中进行维护:线程库通常会维护一个数据结构,比如线程表,用于存储每个线程的信息。这个表中的每个条目可能包含线程的标识符(如线程ID)、状态、优先级、调度相关信息,以及指向线程控制块(TCB)或线程栈的指针等。这样的线程表使得线程库可以有效地跟踪和管理每个线程的状态和资源
重要的是,线程控制块(TCB)通常包含了线程的所有信息,包括线程的上下文、寄存器状态、线程栈指针、局部存储等。因此,线程表中存储的指针指向每个线程的TCB,而TCB中又包含了线程的所有信息,使得线程库可以在需要时访问和修改线程的相关信息