【Linux】线程
一、线程的核心概念
1.1 什么是线程
线程是"进程内部的控制序列",是操作系统调度的最小单位。一个进程至少包含一个主线程,也可以创建多个子线程,所有线程共享进程的地址空间和资源,同时拥有自己的私有上下文。
核心特征:
- 线程在进程地址空间内运行,共享代码段、数据段、文件描述符等资源。
- 线程拥有独立的线程ID、寄存器上下文、栈空间、errno和信号屏蔽字。
- 内核视角:Linux没有专门的线程结构体,线程本质是"轻量级进程",通过
task_struct结构体管理,与进程共享mm_struct(虚拟地址空间)。
1.2 线程与进程的区别与联系
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 资源分配的基本单位 | 调度的基本单位 |
| 地址空间 | 独立地址空间,进程间隔离 | 共享进程地址空间 |
| 创建开销 | 大(需分配地址空间、页表等) | 小(仅需创建线程上下文) |
| 切换开销 | 大(需切换地址空间、刷新TLB) | 小(仅切换寄存器和栈) |
| 通信方式 | 需借助IPC(管道、共享内存等) | 可直接访问进程全局变量 |
| 健壮性 | 进程崩溃不影响其他进程 | 单个线程崩溃导致整个进程终止 |
1.3 线程的优势与缺点
优势
- 开销小:创建和切换线程的成本远低于进程。
- 高并发:多线程可充分利用多CPU核心,提升程序执行效率。
- 资源共享:线程间共享进程资源,通信无需额外IPC机制,效率更高。
- IO密集型优化:线程等待IO时,其他线程可继续执行,提升程序吞吐量。
缺点
- 健壮性低:线程缺乏资源隔离,一个线程的错误(如野指针)会导致整个进程崩溃。
- 编程复杂:需处理线程同步、竞态条件等问题,调试难度高。
- 缺乏访问控制:进程是资源访问控制的基本单位,线程无法单独设置资源权限。
二、线程的内存管理基础
要理解线程,必须先掌握Linux的分页式存储管理和进程地址空间布局,这是线程资源共享与私有隔离的底层基础。
2.1 分页式存储管理
2.1.1 虚拟地址与物理地址映射
- 虚拟地址:操作系统为每个进程分配的连续逻辑地址空间(32位系统为0~4GB)。
- 物理地址:内存硬件的实际地址,离散分配。
- 页表:建立虚拟地址与物理地址的映射关系,将虚拟地址划分为"页"(4KB),物理地址划分为"页框",通过页表实现离散映射。
2.1.2 多级页表与TLB
单级页表会占用大量连续内存(32位系统需4MB),Linux采用二级页表解决该问题:
- 页目录表:存储二级页表的物理地址。
- 页表:存储虚拟页与物理页框的映射。
- TLB(转译后备缓冲器):缓存近期使用的页表项,加速地址转换(减少页表查询次数)。
2.1.3 缺页异常
当CPU访问的虚拟地址未映射到物理页时,会触发缺页异常,内核处理流程:
- 硬缺页:物理内存中无对应页,需从磁盘加载到内存并建立映射。
- 软缺页:物理内存已有对应页(如共享内存),仅需建立映射。
- 无效缺页:地址越界或无权限,触发
SIGSEGV信号终止进程。
2.2 线程的地址空间布局
进程地址空间中,线程的资源分布如下:
- 共享资源:代码段(.text)、数据段(.data/.bss)、堆区、文件描述符表、信号处理方式、当前工作目录。
- 私有资源 :
- 主线程栈:位于进程栈区(从高地址向下生长)。
- 子线程栈:位于共享区(mmap区域),默认大小为8MB,通过
pthread_attr_t可修改。 - 线程控制块(TCB):存储线程ID、栈地址、状态等信息,本质是进程地址空间中的一块内存(
pthread_t类型本质是该地址)。
地址空间布局示意图(从高地址到低地址):
内核空间
├── 主线程栈
├── 共享区(mmap):子线程栈、动态库、线程局部存储(TLS)
├── 堆区(向上生长)
├── .bss段(未初始化全局变量)
├── .data段(已初始化全局变量)
└── .text段(代码段)
二、线程控制
Linux通过POSIX线程库(libpthread)提供线程控制接口,所有函数以pthread_开头,使用时需链接线程库(编译选项-lpthread)。
2.1 线程创建(pthread_create)
c
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void*), void *arg);
- 参数说明 :
thread:输出参数,返回创建的线程ID。attr:线程属性(如栈大小、分离状态),NULL表示使用默认属性。start_routine:线程启动后执行的函数(返回值和参数均为void*)。arg:传递给线程函数的参数。
- 返回值 :成功返回0,失败返回错误码(不设置
errno)。
实战案例:创建子线程
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
// 线程执行函数
void* thread_func(void* arg) {
char* msg = (char*)arg;
while (true) {
std::cout << "子线程[" << pthread_self() << "]:" << msg << std::endl;
sleep(1);
}
return nullptr;
}
int main() {
pthread_t tid;
int ret = pthread_create(&tid, NULL, thread_func, (void*)"hello thread");
if (ret != 0) {
std::cerr << "创建线程失败:" << strerror(ret) << std::endl;
return 1;
}
// 主线程执行
while (true) {
std::cout << "主线程[" << pthread_self() << "]:running" << std::endl;
sleep(1);
}
return 0;
}
编译运行:
bash
g++ thread_create.cpp -o thread_create -lpthread
./thread_create
2.2 线程ID与线程属性
2.2.1 线程ID
pthread_self():获取当前线程的ID(进程内唯一,本质是线程控制块的地址)。- LWP(轻量级进程ID) :内核分配的系统级线程ID,可通过
ps -aL查看。
bash
# 查看线程信息
ps -aL | grep thread_create
# 输出示例:
# PID LWP TTY TIME CMD
# 1234 1234 pts/0 00:00:00 thread_create # 主线程(LWP=PID)
# 1234 1235 pts/0 00:00:00 thread_create # 子线程
2.2.2 线程属性(pthread_attr_t)
常用属性设置:
- 栈大小:
pthread_attr_setstacksize。 - 分离状态:
pthread_attr_setdetachstate(设置线程是否可分离)。
示例:设置线程栈大小
cpp
#include <pthread.h>
#include <iostream>
void* thread_func(void* arg) {
std::cout << "子线程栈大小:" << pthread_get_stacksize_np(pthread_self()) << std::endl;
return nullptr;
}
int main() {
pthread_attr_t attr;
pthread_attr_init(&attr); // 初始化属性
pthread_attr_setstacksize(&attr, 1024*1024); // 设置栈大小为1MB
pthread_t tid;
pthread_create(&tid, &attr, thread_func, nullptr);
pthread_join(tid, nullptr);
pthread_attr_destroy(&attr); // 销毁属性
return 0;
}
2.3 线程终止
线程终止有三种合法方式,避免直接调用exit(会终止整个进程):
- 线程函数
return返回(主线程return等价于exit)。 - 调用
pthread_exit主动终止当前线程。 - 其他线程调用
pthread_cancel取消当前线程。
2.3.1 pthread_exit函数
c
void pthread_exit(void *value_ptr);
value_ptr:线程退出状态,需指向全局变量或堆内存(不能是局部变量)。
2.3.2 pthread_cancel函数
c
int pthread_cancel(pthread_t thread);
- 取消指定线程,被取消线程的退出状态为
PTHREAD_CANCELED。 - 注意:线程可通过设置取消点控制是否响应取消。
2.4 线程等待(pthread_join)
线程退出后,需通过pthread_join回收其资源,否则会产生"僵尸线程",占用系统资源。
c
int pthread_join(pthread_t thread, void **value_ptr);
thread:待等待的线程ID。value_ptr:输出参数,接收线程的退出状态。- 功能:调用线程会阻塞,直到目标线程终止。
实战案例:线程等待与退出状态
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdlib>
// 线程1:return返回
void* thread_ret(void* arg) {
std::cout << "线程1:通过return退出" << std::endl;
int* ret = new int(100);
return (void*)ret;
}
// 线程2:pthread_exit退出
void* thread_exit(void* arg) {
std::cout << "线程2:通过pthread_exit退出" << std::endl;
int* ret = new int(200);
pthread_exit((void*)ret);
}
// 线程3:被取消
void* thread_cancel(void* arg) {
while (true) {
std::cout << "线程3:运行中(可被取消)" << std::endl;
sleep(1);
}
return nullptr;
}
int main() {
pthread_t tid1, tid2, tid3;
void* ret;
// 等待线程1
pthread_create(&tid1, NULL, thread_ret, NULL);
pthread_join(tid1, &ret);
std::cout << "线程1退出状态:" << *(int*)ret << std::endl;
delete (int*)ret;
// 等待线程2
pthread_create(&tid2, NULL, thread_exit, NULL);
pthread_join(tid2, &ret);
std::cout << "线程2退出状态:" << *(int*)ret << std::endl;
delete (int*)ret;
// 取消并等待线程3
pthread_create(&tid3, NULL, thread_cancel, NULL);
sleep(3);
pthread_cancel(tid3);
pthread_join(tid3, &ret);
if (ret == PTHREAD_CANCELED) {
std::cout << "线程3被取消" << std::endl;
}
return 0;
}
2.5 线程分离(pthread_detach)
默认情况下,线程是"可连接的(joinable)",必须通过pthread_join回收资源。若不关心线程退出状态,可将线程设置为"分离态(detached)",线程退出后自动释放资源。
c
int pthread_detach(pthread_t thread);
- 可由其他线程调用,也可线程自分离:
pthread_detach(pthread_self())。 - 注意:分离态线程不能被
pthread_join等待,否则返回错误。
实战案例:线程分离
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
void* thread_func(void* arg) {
pthread_detach(pthread_self()); // 线程自分离
std::cout << "分离态线程:运行中" << std::endl;
sleep(2);
std::cout << "分离态线程:退出" << std::endl;
return nullptr;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
sleep(1); // 等待线程完成分离
// 尝试等待分离态线程(会失败)
int ret = pthread_join(tid, NULL);
if (ret != 0) {
std::cerr << "pthread_join失败:" << strerror(ret) << std::endl;
}
sleep(2); // 等待线程退出
return 0;
}
三、线程的内存布局与内核实现
3.1 线程的地址空间分布
进程地址空间中,线程的私有资源和共享资源分布如下:
- 共享资源 :
- 代码段(.text):进程的可执行代码。
- 数据段(.data/.bss):全局变量和静态变量。
- 堆区:动态分配的内存(
malloc/new分配)。 - 文件描述符表、信号处理方式、当前工作目录。
- 私有资源 :
- 线程栈:主线程栈位于进程栈区(高地址向下生长),子线程栈位于共享区(
mmap分配,默认8MB)。 - 线程控制块(TCB):
pthread库维护的struct pthread结构体,存储线程ID、栈地址、退出状态等信息。 - 线程局部存储(TLS):通过
__thread关键字定义的变量,每个线程有独立副本。
- 线程栈:主线程栈位于进程栈区(高地址向下生长),子线程栈位于共享区(
3.2 内核对线程的实现
Linux内核没有专门的线程结构体,线程本质是"轻量级进程",通过clone系统调用创建,与进程共享mm_struct(虚拟地址空间):
clone系统调用:创建轻量级进程,通过标志位控制资源共享(CLONE_VM共享地址空间、CLONE_FILES共享文件描述符等)。- 内核视角:每个线程对应一个
task_struct,共享mm_struct、files_struct等结构体,实现资源共享。
核心流程:
- 调用
pthread_create时,libpthread库先通过mmap为线程分配栈空间和TCB。 - 调用
clone系统调用,内核创建task_struct,共享进程的mm_struct。 - 线程执行指定的
start_routine函数,运行结束后释放资源。
四、线程封装实战:面向对象的线程类
基于POSIX线程库,封装一个通用的线程类,支持线程创建、启动、等待和分离,简化多线程编程。
4.1 线程类实现(Thread.hpp)
cpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <unistd.h>
namespace ThreadModule {
// 线程状态
enum class ThreadStatus {
NEW, // 未启动
RUNNING, // 运行中
STOPPED // 已停止
};
// 线程函数类型(无参数,无返回值)
using ThreadFunc = std::function<void()>;
class Thread {
public:
// 构造函数:传入线程执行函数
Thread(ThreadFunc func)
: _func(func), _status(ThreadStatus::NEW), _joined(true) {
_name = "Thread-" + std::to_string(_cnt++);
}
// 禁用拷贝构造和赋值
Thread(const Thread&) = delete;
Thread& operator=(const Thread&) = delete;
// 析构函数
~Thread() {
if (_status == ThreadStatus::RUNNING && !_joined) {
pthread_detach(_tid); // 分离未等待的线程,避免资源泄漏
}
}
// 设置线程为分离态
void setDetached() {
if (_status == ThreadStatus::NEW) {
_joined = false;
}
}
// 启动线程
bool start() {
if (_status != ThreadStatus::NEW) {
std::cerr << "线程已启动" << std::endl;
return false;
}
// 创建线程:传入静态成员函数作为入口,this作为参数
int ret = pthread_create(&_tid, nullptr, threadRoutine, this);
if (ret != 0) {
std::cerr << "创建线程失败:" << strerror(ret) << std::endl;
return false;
}
_status = ThreadStatus::RUNNING;
return true;
}
// 等待线程
bool join() {
if (!_joined || _status != ThreadStatus::RUNNING) {
std::cerr << "线程不可等待" << std::endl;
return false;
}
int ret = pthread_join(_tid, nullptr);
if (ret != 0) {
std::cerr << "等待线程失败:" << strerror(ret) << std::endl;
return false;
}
_status = ThreadStatus::STOPPED;
return true;
}
// 获取线程名称
std::string name() const { return _name; }
private:
// 静态成员函数:线程入口(必须是静态函数,无this指针)
static void* threadRoutine(void* arg) {
Thread* self = static_cast<Thread*>(arg);
// 设置线程名称(调试用)
pthread_setname_np(pthread_self(), self->_name.c_str());
// 执行线程函数
self->_func();
self->_status = ThreadStatus::STOPPED;
return nullptr;
}
private:
static std::uint32_t _cnt; // 线程计数器(用于生成名称)
std::string _name; // 线程名称
pthread_t _tid; // 线程ID
ThreadStatus _status; // 线程状态
bool _joined; // 是否可连接(默认true)
ThreadFunc _func; // 线程执行函数
};
// 初始化静态计数器
std::uint32_t ThreadModule::Thread::_cnt = 0;
}
4.2 线程类使用示例(main.cc)
cpp
#include <iostream>
#include <unistd.h>
#include "Thread.hpp"
using namespace ThreadModule;
// 线程1:打印信息
void printInfo() {
while (true) {
std::cout << "线程[" << pthread_self() << "]:" << Thread::current()->name() << " 运行中" << std::endl;
sleep(1);
}
}
// 线程2:计数
void countNum() {
int cnt = 0;
while (cnt < 5) {
std::cout << "线程[" << pthread_self() << "]:计数 " << ++cnt << std::endl;
sleep(1);
}
std::cout << "线程[" << pthread_self() << "]:计数完成" << std::endl;
}
int main() {
// 创建线程1(默认可连接)
Thread t1(printInfo);
t1.start();
std::cout << "启动线程:" << t1.name() << std::endl;
// 创建线程2(分离态)
Thread t2(countNum);
t2.setDetached();
t2.start();
std::cout << "启动线程:" << t2.name() << std::endl;
// 等待线程1(线程2为分离态,无需等待)
t1.join();
return 0;
}
4.3 编译运行
makefile
CC = g++
FLAGS = -std=c++11 -Wall -g
LDFLAGS = -lpthread
TARGET = thread_demo
SRC = main.cc
$(TARGET): $(SRC)
$(CC) $(FLAGS) -o $@ $^ $(LDFLAGS)
.PHONY: clean
clean:
rm -f $(TARGET)
bash
make
./thread_demo
五、线程的关键注意事项
5.1 线程安全与竞态条件
线程共享进程资源,当多个线程同时访问共享变量时,可能导致数据不一致,这就是竞态条件 。确保线程安全的核心是"同步与互斥",常用机制包括互斥锁(pthread_mutex_t)、条件变量(pthread_cond_t)等。
示例:竞态条件问题
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
int g_cnt = 0; // 共享全局变量
void* increment(void* arg) {
for (int i = 0; i < 10000; ++i) {
g_cnt++; // 非原子操作,可能导致竞态条件
}
return nullptr;
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, increment, NULL);
pthread_create(&tid2, NULL, increment, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
std::cout << "最终计数:" << g_cnt << std::endl; // 可能小于20000
return 0;
}
5.2 可重入函数
可重入函数是指多个线程同时调用时,不会因共享资源导致数据错乱的函数。线程安全的函数不一定是可重入的,但可重入函数一定是线程安全的。
不可重入函数的特征:
- 访问全局变量、静态变量或共享资源。
- 调用
malloc/free(堆内存由全局链表管理)。 - 使用标准I/O库函数(如
printf,共享缓冲区)。
5.3 线程局部存储(TLS)
线程局部存储用于存储线程私有数据,通过__thread关键字定义,每个线程有独立副本,避免线程安全问题。
示例:线程局部存储
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
__thread int t_local = 0; // 线程局部变量
void* thread_func(void* arg) {
t_local = *(int*)arg;
std::cout << "线程[" << pthread_self() << "]:t_local = " << t_local << std::endl;
return nullptr;
}
int main() {
int a = 10, b = 20;
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, thread_func, &a);
pthread_create(&tid2, NULL, thread_func, &b);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
六、总结
Linux线程是实现并发编程的核心技术,核心要点如下:
- 线程共享进程地址空间,拥有独立的上下文,创建和切换开销小。
- 线程控制依赖POSIX线程库,核心API包括
pthread_create、pthread_join、pthread_exit等。 - 线程的内核实现本质是轻量级进程,通过
clone系统调用创建,共享mm_struct。 - 线程安全是多线程编程的核心问题,需通过同步机制(互斥锁、条件变量)避免竞态条件。
- 实战中可封装线程类,简化线程的创建、启动和管理。