跳到主要内容
Linux 编程中常用的控制线程函数详解 | 极客日志
C++
Linux 编程中常用的控制线程函数详解 综述由AI生成 Linux 多线程编程核心函数包括 pthread_create 创建线程、pthread_self 获取 ID、pthread_exit 终止线程、pthread_join 等待线程结束及返回值、pthread_detach 分离线程状态以及 pthread_cancel 取消线程。文章解析了 Linux 下线程本质为轻量级进程(LWP),通过 pthread 库封装 clone 系统调用实现。代码示例展示了线程崩溃影响、ID 转换及状态管理细节,强调主线程需处理子线程资源回收以避免僵尸进程,并阐明用户态线程 ID 与内核态 LWP 的区别。
筑梦师 发布于 2026/3/14 更新于 2026/6/10 16 浏览绪论
本章将结合代码和逻辑图的方式带你去认识和了解控制线程中常用的函数,这些函数对后续的开发以及对线程底层的了解都非常重要。
1. 线程控制的函数
1.1 创建线程:pthread_create
头文件:#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;
= function< ()>;
{
:
( string& name, & ctime, f) : (name), (ctime), (f) {}
:
string threadname;
createtime;
func;
};
{
ThreadData* td = <ThreadData*>(arg);
( ) {
td-> ();
cout << << td->threadname << << td->createtime << endl;
( );
(td->threadname == )
{
cout << td->threadname << << endl;
i = ; i /= ;
}
}
}
{ cout << ; }
cnt = ;
{
cout << << endl;
( i = ; i < cnt; i++)
{
( );
tname[ ];
(tname, (tname), , , i);
ThreadData* td = (tname, ( ) ( ), Print);
tid;
(&tid, , ThreadRoutine, td);
}
( ) {
( );
}
;
}
using
func_t
void
class
ThreadData
public
ThreadData
const
const
uint64_t
func_t
threadname
createtime
func
public
uint64_t
func_t
void * ThreadRoutine (void * arg)
static_cast
while
true
func
"threadname: "
" create time: "
sleep
1
if
"thread-4"
" create exeption"
int
1
0
void Print ()
"I am Thread of part: "
const
int
5
int main ()
"main thread"
for
int
0
sleep
1
char
64
snprintf
sizeof
"%s-%d"
"thread"
new
ThreadData
uint64_t
time
nullptr
pthread_t
pthread_create
nullptr
while
true
sleep
10
return
0
最终结果如下图(4 号线程崩溃导致所有线程崩溃)。
1.2 获取线程 id:pthread_self 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 到底是什么)。
1.3 线程的终止 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) 是用来终止的进程(不能作为线程的一种),否则整个进程都会被终止!
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 );
}
pthread_exit (nullptr );
}
1.4 线程的返回
线程返回的时候默认是要被等待的
进程直接退出,没有等待线程的话,会导致类似进程的僵尸问题
1.4.1 线程等待的函数: int pthread_join (pthread_t thread, void ** retval) ;
thread:线程的 id
retval:一个输出型参数,用于获取线程的返回值(该参数得到线程返回值 void*,外部要得到就得使用 void**),也就是等待后其参数二会接收来自对应线程的返回值。
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" ;
}
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* 所以可以返回的任意类型的(包括对象),外部使用强转回来即可使用。
1.5 线程的状态
joinable 状态:线程默认为 joinable 的,主线程会在 pthread_join 处阻塞式的等待线程。
分离状态:就不用等待了主线程就不用管线程,线程运行结束就自动退出了。(不等待)
状态可以理解成:一个家庭,家庭成员他们相互有关也就是 joinable 的(你的事情父母会管),而分离状态也就相当于儿子和父母的关系不好分家了,他们就互不关心了(资源隔离)。
不过线程再怎么分离,资源还是多线程共用的,一个线程出问题别的线程也会出错。
那么状态的不同就引出了不同的情况,也就引出了不同状态时使用的不同函数。
1.5.1 线程分离的函数: 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) {
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 ;
}
1.6 取消(终止)线程函数 pthread_cancel int pthread_cancel (pthread_t thread) ;
线程如果是被分离的,该线程仍可以被 pthread_cancel 取消,但不能被 pthread_join 等待。
并且主线程也是能取消的,取消后主线程内的代码将不会被执行。
取消后等待的退出码结果是 -1(-1 就表明线程是被取消掉的)
线程通过调用 pthread_cancel 异常终止,retval(pthread_join 的第二个参数)所指向的单元里存放的是常量 PTHREAD_CANCELED
2. 线程的 id 和 LWP 到底是什么?
Linux 中所有的线程接口都不是系统直接提供的接口,而是原生线程库 pthread 提供的接口
每个操作系统的任意版本都必须默认配备一个该库,否则多线程在 Linux 下跑步起来。
用户成若有 5 个线程,那么本质上就是在内核系统中就会有 5 个 LWP
对此所有线程(轻量级进程)都需要被管理,所以在中间的 pthread 库也需要能管理系统中的 LWP
Linux 的线程一般称为用户级线程(因为本质线程只是在用户层实现,底层是轻量级进程)
其实线程的 id 本质是原生库中的概念,而 LWP 是系统底层的概念,在 Linux 下在内核态和用户态见有一个线程库(他本质是为了给没学过 LWP 的人也能方便的使用的),其中线程库中有他自己的 id(并且线程库是能找到对应线程 tcb 的),而底层使用的就是 LWP,也就是通过 id 找到对应的 tcb(id 只是用于找到对应的 tcb 的他其实用的就是 LWP),详细见 3.2。
3. 底层系统调用细节
上下文(寄存器中存储)
栈结构,只有一个栈(只有一套寄存器),但有多个堆
3.1 创建轻量级进程 int clone (int (*fn)(void *), void * child_stack, int flags, void * arg, ...) ;
fn:是所要执行的方法(回调函数)
child_stack:是申请所要使用的栈的空间地址(允许用户传入栈空间)
flag:用来区分创建的是进程还是轻量级进程
arg:是用来给第一个函数传参的
pthread 创建线程的本质就是通过函数 clone 创建的:
也就表示
pthread_create() 的底层是封装了 clone 的
clone 它同样也是 fork 的底层。
child_stack 可以通过 malloc 来申请空间,所以每个新线程的栈都是在库中维护,其中线程库在内存共享区有指向自己的栈空间的地址变量。而默认地址空间的栈,由主线程使用。
3.2 如何理解 pthread 库来管理线程 一个操作系统可能有多个进程,而这些进程他们所使用的都是同一个相同的库,所以称他为共享的!
并且线程是在用户地址空间也就是共享区(内存映射段,不了解的可以搜一下内存模型)
其中 mmap 区就是在内存映射段的(存储着线程库)
mmap 区域的动态库中就包含有线程库,其中当一个线程的创建就会再动态库中创建一个线程的属性集:
线程的创建和管理通常由线程库(如 pthread)抽象化实现,底层依赖于 clone 系统调用。使用 clone 创建线程时,需要提供用户函数(线程执行的函数)、栈地址和一组标志位,这些标志位决定了线程之间共享的资源(如地址空间、文件描述符等)。线程的属性集由线程库维护,底层 clone 系统调用完成轻量级进程的创建。
当线程完成执行时,可以通过返回一个 void* 值向调用方传递结果。通过调用 pthread_join,可以等待线程完成,并获取其返回值。线程库维护一个类似数组的结构,用于存储每个线程的属性集(如线程 ID、栈地址、返回值等)。线程 ID(tid)可作为索引,用于高效管理和调度线程。
每个线程的属性集可以看作数组中的一个元素。线程库在底层以动态数组的形式维护这些属性集,随着线程的创建,不断扩展数组结构,以便支持更多的线程。
具体如下图(类似于数组的底层):
所以我们所用的线程 id,它就是线程属性集合在库中的地址 (每个属性集的起始地址)!!!所以 pthread_join 通过线程 id 就能很好的拿到想要拿到的线程的数据。
LWP 和线程 id 不一样是因为:LWP 是内核的概念,而 tid 是线程库里的概念(人们不用理解 LWP)。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online