绪论
本章将结合代码和逻辑图的方式带你去认识和了解控制线程中常用的函数,这些函数对后续的开发以及对线程底层的了解都非常重要。
Linux 多线程编程核心函数包括 pthread_create 创建线程、pthread_self 获取 ID、pthread_exit 终止线程、pthread_join 等待线程结束及返回值、pthread_detach 分离线程状态以及 pthread_cancel 取消线程。文章解析了 Linux 下线程本质为轻量级进程(LWP),通过 pthread 库封装 clone 系统调用实现。代码示例展示了线程崩溃影响、ID 转换及状态管理细节,强调主线程需处理子线程资源回收以避免僵尸进程,并阐明用户态线程 ID 与内核态 LWP 的区别。

本章将结合代码和逻辑图的方式带你去认识和了解控制线程中常用的函数,这些函数对后续的开发以及对线程底层的了解都非常重要。
头文件:#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void* (*start_routine)(void*), void *arg);
参数说明:
thread:线程 ID(类似于进程的 PID,输出型)attr:线程的属性(暂时不管,统一写成 nullptr)start_routine:函数指针,类型为 void* (*)(void*),代表多线程执行的函数(自定义任务)arg:用来给第三个函数指针传参数的(因为其类型是 void*,所以可以传递任意类型进去,包括对象)Linux 中本质是没有线程的,只有轻量级进程的概念,所以 Linux OS 只会提供轻量级进程创建的系统调用,不会直接提供线程的创建接口。在学习操作系统时只学过操作系统中的线程,没有学过 Linux 中轻量级进程(LWP)概念的人也能正常的使用,就必须得在操作系统和用户之间重新写一个 pthread 原生线程库让所有人都能正常使用通过线程,而底层其实是轻量级进程。
既然它是一个第三方库,那在编译时就需要接入外部库,通过 -l 附加指令(-l 库名字)。所以通过编译器 g++,需要附加:-lpthread 引入线程库。
注:其中如何传参给第三个参数的函数:这个函数的参数类型是 void*,也就表示能接受任何类型,在内部使用时进行强转即可。
使用上面函数以及证明一个线程崩溃会影响所有线程崩溃:
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <functional>
using namespace std;
using func_t = function<void()>;
// function 创建了一个接收函数签名 void() 的包装器类型,他能接收该类型的函数(void:返回值,():函数没有参数)
// 创建一个对象,其中包含了线程名,线程创建的时间,该线程所要执行的函数
class ThreadData {
public:
ThreadData(const string& name, const uint64_t& ctime, func_t f) : threadname(name), createtime(ctime), func(f) {}
public:
string threadname; // 线程名
uint64_t createtime; // 创建时间
func_t func; // 接收函数(void())的包装器
};
void* ThreadRoutine(void* arg) // 创建一个线程后调用的函数,其中 arg 是参数,通过第四个参数传递进来
{
ThreadData* td = static_cast<ThreadData*>(arg); // 进行类型的强转成,对象
while (true) {
td->func(); // 对象中存着,真的调用的函数!
cout << "threadname: " << td->threadname << " create time: " << td->createtime << endl; // 通过对象访问成员变量
sleep(1);
if (td->threadname == "thread-4") // 在 4 号进处出产生信号,观察情况
{
cout << td->threadname << " create exeption" << endl;
int i = 1; i /= 0; // 让 4 号线程进来时发生除零异常导致崩溃看看是否会影响
}
}
}
void Print() { cout << "I am Thread of part: "; }
const int cnt = 5;
int main()
{
cout << "main thread" << endl;
for (int i = 0; i < cnt; i++) // 主线程循环多次,创建多个线程
{
sleep(1);
char tname[64];
snprintf(tname, sizeof(tname), "%s-%d", "thread", i); // 让每个下标对应一个进程名 tname
ThreadData* td = new ThreadData(tname, (uint64_t)time(nullptr), Print); // 创建一个对象,并初始化
// time 函数获取当前时间(传递空)
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, td); // 创建进程
}
while (true) {
sleep(10);
}
return 0;
}
最终结果如下图(4 号线程崩溃导致所有线程崩溃)。
头文件:#include <pthread.h>
pthread_t pthread_self(void);
实操代码:
using func_t = function<void()>;
string ToHex(pthread_t tid) {
char id[64];
snprintf(id, sizeof(id), "0x%lx", tid);
return id;
}
void* ThreadRoutine(void* arg) {
string threadname = static_cast<const char*>(arg);
while (true) {
cout << "new thread name: " << threadname << " thread id: " << ToHex(pthread_self()) << endl;
sleep(1);
}
}
int main() {
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread-1");
while (true) {
cout << "main thread , sub thread " << tid << " main thread id: " << ToHex(pthread_self()) << endl;
sleep(1);
}
return 0;
}
其中不难发现线程 id 值非常大,把它通过自定义函数 ToHex 转成十六进制来看,发现它很像一个地址,那他是地址吗?
再通过 ps -aL 查看线程的 LWP 发现和线程 id 并不一样?
上述两问题,我们继续往下看,后面通过底层分析解释(因为内容较多,见 2.线程 id 和 LWP 到底是什么)。
string ToHex(pthread_t tid) {
char id[64];
snprintf(id, sizeof(id), "0x%x", tid);
return id;
}
void* ThreadRoutine(void* arg) {
string threadname = static_cast<const char*>(arg);
int cnt = 5;
while (cnt--) {
cout << "new thread name: " << threadname << " thread id: " << ToHex(pthread_self()) << endl;
sleep(1);
}
return nullptr;
}
int main() {
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread-1");
while (true) {
cout << "main thread , sub thread " << ToHex(tid) << " main thread id: " << ToHex(pthread_self()) << endl;
sleep(1);
}
return 0;
}
其中注意的是:exit(int) 是用来终止的进程(不能作为线程的一种),否则整个进程都会被终止!
头文件:#include <pthread.h>
void* ThreadRoutine(void* arg) {
string threadname = static_cast<const char*>(arg);
int cnt = 5;
while (cnt--) {
cout << "new thread name: " << threadname << " thread id: " << ToHex(pthread_self()) << endl;
sleep(1);
}
// return nullptr;
pthread_exit(nullptr);
}
这里替代上面代码中的同位置函数即源码。
那么就继续引出下面等待函数
头文件:#include <pthread.h>
int pthread_join(pthread_t thread, void** retval);
thread:线程的 idretval:一个输出型参数,用于获取线程的返回值(该参数得到线程返回值 void*,外部要得到就得使用 void**),也就是等待后其参数二会接收来自对应线程的返回值。返回值:如果等待成功返回 0,失败则返回错误码
using func_t = function<void()>;
string ToHex(pthread_t tid) {
char id[64];
snprintf(id, sizeof(id), "0x%x", tid);
return id;
}
void* ThreadRoutine(void* arg) {
string threadname = static_cast<const char*>(arg);
int cnt = 5;
while (cnt--) {
cout << "new thread name: " << threadname << " thread id: " << ToHex(pthread_self()) << endl;
sleep(1);
}
return (void*)"thread-done"; // 返回的是这个字符串常量的起始地址
// pthread_exit(nullptr);
}
int main() {
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread-1");
cout << " main thread id: " << ToHex(pthread_self()) << endl;
sleep(8);
void* ret = nullptr;
int n = pthread_join(tid, &ret);
cout << "main thread get new thread return: " << (const char*)ret << endl; // 获取线程的返回值,并打印
return 0;
}
返回用 ret 接收,因为其类型是 void*,进行强转为 const char* 打印出来。
同理因为返回的是 void* 所以可以返回的任意类型的(包括对象),外部使用强转回来即可使用。
状态可以理解成:一个家庭,家庭成员他们相互有关也就是 joinable 的(你的事情父母会管),而分离状态也就相当于儿子和父母的关系不好分家了,他们就互不关心了(资源隔离)。 不过线程再怎么分离,资源还是多线程共用的,一个线程出问题别的线程也会出错。
那么状态的不同就引出了不同的情况,也就引出了不同状态时使用的不同函数。
头文件:#include <pthread.h>
int pthread_detach(pthread_t thread);
该函数可以在线程内使用,也可以直接在主线程内使用(参数指定要分离的线程 id)。
void* ThreadRoutine(void* arg) {
pthread_detach(pthread_self());
int cnt = 5;
while (cnt--) {
cout << "new thread runing... " << endl;
sleep(1);
}
return nullptr;
}
int main() {
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread-1");
sleep(1);
int n = pthread_join(tid, nullptr);
cout << "main thread get new thread return: " << n << endl;
return 0;
}
其中返回错误码 22 是通用的错误代码,在不同的编程语言、系统或环境中可能有不同的含义。通常,它对应的描述是 Invalid argument(无效参数)。
通过上图,可以发现打印好像出了点问题,但其实本质就体现了线程的分离状态,线程和主进程分离了,也就形成了异步执行打印的操作,这样才出来点问题。
让我们继续看不分离的状态,也就是 joinable 状态(默认就是,所以注释分离函数即可)
void* ThreadRoutine(void* arg) {
// pthread_detach(pthread_self());
int cnt = 5;
while (cnt--) {
cout << "new thread runing... " << endl;
sleep(1);
}
return nullptr;
}
int main() {
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread-1");
sleep(1);
pthread_detach(tid);
int n = pthread_join(tid, nullptr);
cout << "main thread get new thread return: " << n << endl;
return 0;
}
头文件:#include <pthread.h>
int pthread_cancel(pthread_t thread);
成功返回 0,反之返回错误码
线程如果是被分离的,该线程仍可以被 pthread_cancel 取消,但不能被 pthread_join 等待。
并且主线程也是能取消的,取消后主线程内的代码将不会被执行。
取消后等待的退出码结果是 -1(-1 就表明线程是被取消掉的)
线程通过调用 pthread_cancel 异常终止,retval(pthread_join 的第二个参数)所指向的单元里存放的是常量 PTHREAD_CANCELED
首先了解线程中的概念:
线程和系统的关系
Linux 的线程一般称为用户级线程(因为本质线程只是在用户层实现,底层是轻量级进程)
这样我们就能知道:
其实线程的 id 本质是原生库中的概念,而 LWP 是系统底层的概念,在 Linux 下在内核态和用户态见有一个线程库(他本质是为了给没学过 LWP 的人也能方便的使用的),其中线程库中有他自己的 id(并且线程库是能找到对应线程 tcb 的),而底层使用的就是 LWP,也就是通过 id 找到对应的 tcb(id 只是用于找到对应的 tcb 的他其实用的就是 LWP),详细见 3.2。
线程的独立属性:
头文件:#include <sched.h>
int clone(int (*fn)(void*), void* child_stack, int flags, void* arg, ...);
fn:是所要执行的方法(回调函数)child_stack:是申请所要使用的栈的空间地址(允许用户传入栈空间)flag:用来区分创建的是进程还是轻量级进程arg:是用来给第一个函数传参的pthread 创建线程的本质就是通过函数 clone 创建的: 也就表示
child_stack 可以通过 malloc 来申请空间,所以每个新线程的栈都是在库中维护,其中线程库在内存共享区有指向自己的栈空间的地址变量。而默认地址空间的栈,由主线程使用。
一个操作系统可能有多个进程,而这些进程他们所使用的都是同一个相同的库,所以称他为共享的!
并且线程是在用户地址空间也就是共享区(内存映射段,不了解的可以搜一下内存模型)
其中 mmap 区就是在内存映射段的(存储着线程库)
mmap 区域的动态库中就包含有线程库,其中当一个线程的创建就会再动态库中创建一个线程的属性集:
struct pthread 会存着线程的属性:
线程的创建和管理通常由线程库(如 pthread)抽象化实现,底层依赖于 clone 系统调用。使用 clone 创建线程时,需要提供用户函数(线程执行的函数)、栈地址和一组标志位,这些标志位决定了线程之间共享的资源(如地址空间、文件描述符等)。线程的属性集由线程库维护,底层 clone 系统调用完成轻量级进程的创建。
当线程完成执行时,可以通过返回一个 void* 值向调用方传递结果。通过调用 pthread_join,可以等待线程完成,并获取其返回值。线程库维护一个类似数组的结构,用于存储每个线程的属性集(如线程 ID、栈地址、返回值等)。线程 ID(tid)可作为索引,用于高效管理和调度线程。
每个线程的属性集可以看作数组中的一个元素。线程库在底层以动态数组的形式维护这些属性集,随着线程的创建,不断扩展数组结构,以便支持更多的线程。 具体如下图(类似于数组的底层):
所以我们所用的线程 id,它就是线程属性集合在库中的地址 (每个属性集的起始地址)!!!所以 pthread_join 通过线程 id 就能很好的拿到想要拿到的线程的数据。
LWP 和线程 id 不一样是因为:LWP 是内核的概念,而 tid 是线程库里的概念(人们不用理解 LWP)。
本章完。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online