3. Linux 线程控制:POSIX 线程库的核心操作
Linux 中通过 **POSIX 线程库(pthread)** 实现线程控制,包括线程创建、终止、等待、分离等操作。这些操作是多线程编程的基础,本节详解其用法与原理。
3-1 POSIX 线程库基础
POSIX 线程库是 Linux 下多线程编程的标准接口,核心特点:
- 函数命名 :线程相关函数以
pthread_
开头(如pthread_create
、pthread_join
)。 - 头文件 :需包含
<pthread.h>
。 - 编译链接 :pthread是第三方库,所以编译时需加
-lpthread
选项(链接 pthread 库)。
3-2 创建线程:pthread_create
功能
创建新线程,使其执行指定函数。
函数原型
cpp
int pthread_create(
pthread_t *thread, // 输出参数:返回线程ID
const pthread_attr_t *attr, // 线程属性(NULL表示默认属性)
void *(*start_routine)(void*), // 线程执行的函数(函数指针)
void *arg // 传递给线程函数的参数
);
返回值
- 成功:返回
0
。 - 失败:返回错误码 (非
-1
,pthread 函数不设置errno
)。
示例
cpp
#include<iostream>
#include<pthread.h>
#include<unistd.h>
void* threadrun(void* args)
{
std::string name = (const char*)args;
while(1)
{
std::cout << "我是新线程" << name << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
int n = pthread_create(&tid, nullptr, threadrun, (void*)"thread-1");
if(n != 0)
{
std::cout << "pthread_create error" << std::endl;
return 1;
}
while(1)
{
std::cout << "我是主线程" << std::endl;
sleep(1);
}
return 0;
}

- 内核线程 ID(LWP) :通过
ps -aL
查看,是内核调度的唯一标识(系统全局唯一)。

CPU调度的时候,看得实际是lwp。
3-3 线程终止
线程可通过以下 3 种方式终止,且仅终止自身(不影响进程内其他线程):
1. 线程函数return
线程函数执行到return
时,线程自然终止(主线程return
会导致进程退出)。
2. pthread_exit
主动终止
cpp
void pthread_exit(void *value_ptr);
value_ptr
:线程退出时的返回值(需为全局 / 堆内存,不能是栈变量)。- 无返回值(线程自身无法 "返回" 到调用者)。
3. pthread_cancel
取消其他线程
cpp
int pthread_cancel(pthread_t thread);
- 向目标线程发送 "取消请求",目标线程在取消点 (如
sleep
、read
等系统调用)响应并终止。 - 返回值:成功
0
,失败错误码。
3-4 线程等待:pthread_join
在 POSIX 线程(pthread
)中,若创建的非分离态线程未通过 pthread_join
等待回收,会导致僵尸线程,引发以下问题:
-
资源泄漏:线程结束后,内核中的线程控制块(TCB)不会释放,持续占用内核内存和线程描述符等稀缺资源。大量僵尸线程会耗尽系统线程资源,导致无法创建新线程。
-
内存泄漏 :线程分配的私有资源(如堆内存、文件描述符)若依赖
pthread_join
回收(例如通过线程返回值传递资源指针),不调用该函数会导致这些资源无法释放,造成用户态内存泄漏。 -
程序逻辑隐患 :
pthread_join
可获取线程退出状态,不调用则主线程无法得知子线程是否正常结束、是否返回预期结果,可能引发逻辑错误。
功能
等待指定线程终止,回收其资源(类似进程的wait
)。
函数原型
cpp
int pthread_join(
pthread_t thread, // 要等待的线程ID
void **value_ptr // 输出参数:存储线程的退出状态
);
线程退出状态的获取
- 若线程通过
return
退出:value_ptr
存储线程函数的返回值。 - 若线程被
pthread_cancel
终止:value_ptr
存储PTHREAD_CANCELED
。 - 若线程调用
pthread_exit
:value_ptr
存储pthread_exit
的参数。
示例
cpp
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
void *thread1(void *arg) {
int *ret = malloc(sizeof(int));
*ret = 100;
return (void *)ret; // return退出,返回值存于堆
}
void *thread2(void *arg) {
int *ret = malloc(sizeof(int));
*ret = 200;
pthread_exit((void *)ret); // pthread_exit退出
}
void *thread3(void *arg) {
while (1) sleep(1); // 无限循环,等待被取消
}
int main() {
pthread_t tid;
void *ret;
// 测试return退出
pthread_create(&tid, NULL, thread1, NULL);
pthread_join(tid, &ret);
printf("Thread 1 return: %d\n", *(int *)ret);
free(ret);
// 测试pthread_exit退出
pthread_create(&tid, NULL, thread2, NULL);
pthread_join(tid, &ret);
printf("Thread 2 exit: %d\n", *(int *)ret);
free(ret);
// 测试pthread_cancel退出
pthread_create(&tid, NULL, thread3, NULL);
sleep(1);
pthread_cancel(tid);
pthread_join(tid, &ret);
if (ret == PTHREAD_CANCELED)
printf("Thread 3 canceled\n");
return 0;
}
3-5 分离线程:pthread_detach
背景
默认情况下,线程是joinable 的:若不调用pthread_join
,线程终止后资源不会自动释放,导致 "僵尸线程"。
功能
将线程标记为分离态(detached) ,线程终止时自动释放资源,无需pthread_join
。
函数原型
cpp
int pthread_detach(pthread_t thread);
两种分离方式
-
其他线程分离目标线程 :
cpppthread_detach(tid); // tid为目标线程ID
-
线程自身分离 :
cpppthread_detach(pthread_self());
示例
cpp
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *thread_func(void *arg) {
pthread_detach(pthread_self()); // 自身分离
printf("Thread is running...\n");
sleep(1);
return NULL; // 终止后自动释放资源
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
sleep(2); // 等待线程执行
// 无需pthread_join,线程已自动释放资源
printf("Main thread exit\n");
return 0;
}
总结
- 创建 :
pthread_create
启动新线程,共享进程资源。 - 终止 :
return
、pthread_exit
、pthread_cancel
三种方式,灵活控制线程生命周期。 - 等待 :
pthread_join
回收线程资源,获取退出状态。 - 分离 :
pthread_detach
让线程自动释放资源,避免僵尸线程。
这些操作是 Linux 多线程编程的基础,需根据场景选择合适的线程控制方式。
4. 线程 ID 及进程地址空间布局
线程 ID 存在两种不同的 "身份标识":一种是内核调度层面的线程 ID(LWP),另一种是线程库(NPTL)维护的用户态线程 ID。两者作用域和含义不同,需结合进程地址空间布局理解其本质。
4.1 两种线程 ID 的区分
1. 内核线程 ID(LWP)
- 本质:内核级别的线程标识,是操作系统调度器识别线程的唯一标识(类似进程 ID,但针对线程)。
- 特性 :
- 系统全局唯一(整个系统中不会有两个线程拥有相同的 LWP)。
- 属于进程调度范畴,内核通过 LWP 进行线程的调度和资源分配。
- 可通过
ps -aL
命令查看(LWP
列即为内核线程 ID)。
- 示例:主线程的 LWP 与进程 ID(PID)相同,其他线程的 LWP 为独立数值。
2. 用户态线程 ID(pthread_t)
- 本质:由 POSIX 线程库(NPTL,Native POSIX Thread Library)维护的线程标识,是用户态操作线程的 "句柄"。
- 特性 :
- 仅在进程内唯一 (不同进程可存在相同的
pthread_t
值)。 - 属于线程库范畴,线程库的所有操作(如
pthread_join
、pthread_cancel
)均依赖此 ID。 - 通过
pthread_self()
函数获取当前线程的pthread_t
。
- 仅在进程内唯一 (不同进程可存在相同的
- 实现细节 :在 Linux 的 NPTL 实现中,
pthread_t
本质是进程地址空间中的一个地址 (指向线程控制块TCB
的指针),线程库通过该地址访问线程的私有数据(如栈、寄存器上下文等)。
4.2 线程 ID 与进程地址空间的关系
进程地址空间为线程提供了共享与隔离的基础,线程 ID 的存储和使用与地址空间布局紧密相关:
进程地址空间布局


线程私有数据在地址空间中的位置
- 主线程:栈位于进程地址空间的传统栈区(低地址附近),与进程的栈共享同一片区域。
- 其他线程 :栈由 pthread 库在共享库区域与堆之间动态分配(独立于主线程栈),保证线程栈的私有性。
- 线程控制块(TCB) :存储线程 ID(
pthread_t
指向的地址)、寄存器上下文、信号屏蔽字等私有信息,通常位于线程栈的顶部或单独的内存区域,属于进程地址空间的共享区(线程库管理)。
4.3 为何pthread_t
是地址?
Linux 的 NPTL 线程库通过 "地址即标识" 的设计简化线程管理:
pthread_t
直接指向线程的 TCB(线程控制块),线程库可通过该地址快速访问线程的私有数据(如栈指针、状态标志)。- 无需额外的全局表维护 ID 与线程的映射关系,减少了操作开销(如
pthread_join
可直接通过pthread_t
找到线程的退出状态)。 - 进程地址空间的独立性保证了
pthread_t
在进程内的唯一性(不同进程的地址空间隔离,同一地址在不同进程中对应不同线程)。
总结
- 内核线程 ID(LWP):系统级唯一标识,用于内核调度,全局可见。
- 用户态线程 ID(pthread_t):进程内唯一标识,本质是地址(指向 TCB),用于线程库操作。
- 地址空间角色 :线程共享进程的代码段、数据段、堆等,但其栈和 TCB(含
pthread_t
)位于私有区域,保证了执行独立性。
理解两种线程 ID 的区别,是掌握多线程调试(如gdb
查看线程)和线程库原理的关键。
5. 线程封装
Thread.hpp
cpp
#ifndef THREAD_HPP
#define THREAD_HPP
#include <iostream>
#include <string>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <functional>
namespace ThreadModlue
{
static uint32_t number = 1;
class Thread
{
using func_t = std::function<void()>;
private:
void EnableDetach()
{
std::cout << "线程被分离了" << std::endl;
_isdetach = true;
}
void EnableRunning()
{
_isrunning = true;
}
static void *Routine(void *args)
{
Thread *self = static_cast<Thread *>(args);
self->EnableDetach();
if (self->_isdetach)
self->Detach();
pthread_setname_np(self->_tid, self->_name.c_str());
self->_func();
return nullptr;
}
public:
Thread(func_t func)
: _tid(0),
_isdetach(false),
_isrunning(false),
res(nullptr),
_func(func)
{
_name = "Thread-" + std::to_string(number++);
}
void Detach()
{
if (_isdetach)
return;
if (_isrunning)
pthread_detach(_tid);
EnableDetach();
}
bool Start()
{
if (_isrunning)
return false;
int n = pthread_create(&_tid, nullptr, Routine, this);
if (n != 0)
{
std::cerr << "Failed to create thread: " << _name << std::endl;
return false;
}
else
{
std::cout << _name << " started" << std::endl;
}
return true;
}
bool Stop()
{
if (_isrunning)
{
int n = pthread_cancel(_tid);
if (n != 0)
{
std::cerr << "Failed to cancel thread: " << _name << std::endl;
return false;
}
else
{
_isrunning = false;
std::cout << _name << " stopped" << std::endl;
}
}
return true;
}
void Join()
{
if (_isdetach)
return;
int n = pthread_join(_tid, &res);
if (n != 0)
{
std::cerr << "Failed to join thread: " << _name << std::endl;
}
else
{
std::cout << "Join successful" << std::endl;
}
}
~Thread()
{
}
private:
pthread_t _tid;
std::string _name;
bool _isdetach;
bool _isrunning;
void *res;
func_t _func;
};
}
#endif
Main.cc
cpp
#include "Thread.hpp"
#include <unistd.h>
#include <vector>
using namespace ThreadModlue;
int main()
{
std::vector<Thread> threads;
for (int i = 0; i < 10; i++)
{
threads.emplace_back([]()
{
while(true)
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
std::cout << "我是一个新线程: " << name << std::endl;
sleep(1);
} });
}
for (auto &thread : threads)
{
thread.Start();
}
for (auto &thread : threads)
{
thread.Join();
}
return 0;
}