【Linux】线程概念与控制(2)
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
功能
创建新线程,使其执行指定函数。
函数原型
int pthread_create(pthread_t *thread, // 输出参数:返回线程IDconst pthread_attr_t *attr, // 线程属性(NULL表示默认属性)void *(*start_routine)(void*), // 线程执行的函数(函数指针)void *arg // 传递给线程函数的参数
);
返回值
- 成功:返回
0
。 - 失败:返回错误码(非
-1
,pthread 函数不设置errno
)。
示例
#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
主动终止
void pthread_exit(void *value_ptr);
value_ptr
:线程退出时的返回值(需为全局 / 堆内存,不能是栈变量)。- 无返回值(线程自身无法 “返回” 到调用者)。
3. pthread_cancel
取消其他线程
int pthread_cancel(pthread_t thread);
- 向目标线程发送 “取消请求”,目标线程在取消点(如
sleep
、read
等系统调用)响应并终止。 - 返回值:成功
0
,失败错误码。
3-4 线程等待:pthread_join
在 POSIX 线程(pthread
)中,若创建的非分离态线程未通过 pthread_join
等待回收,会导致僵尸线程,引发以下问题:
资源泄漏:线程结束后,内核中的线程控制块(TCB)不会释放,持续占用内核内存和线程描述符等稀缺资源。大量僵尸线程会耗尽系统线程资源,导致无法创建新线程。
内存泄漏:线程分配的私有资源(如堆内存、文件描述符)若依赖
pthread_join
回收(例如通过线程返回值传递资源指针),不调用该函数会导致这些资源无法释放,造成用户态内存泄漏。程序逻辑隐患:
pthread_join
可获取线程退出状态,不调用则主线程无法得知子线程是否正常结束、是否返回预期结果,可能引发逻辑错误。
功能
等待指定线程终止,回收其资源(类似进程的wait
)。
函数原型
int pthread_join(pthread_t thread, // 要等待的线程IDvoid **value_ptr // 输出参数:存储线程的退出状态
);
线程退出状态的获取
- 若线程通过
return
退出:value_ptr
存储线程函数的返回值。 - 若线程被
pthread_cancel
终止:value_ptr
存储PTHREAD_CANCELED
。 - 若线程调用
pthread_exit
:value_ptr
存储pthread_exit
的参数。
示例
#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
。
函数原型
int pthread_detach(pthread_t thread);
两种分离方式
- 其他线程分离目标线程:
pthread_detach(tid); // tid为目标线程ID
- 线程自身分离:
pthread_detach(pthread_self());
示例
#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
#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
#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;
}