《Linux线程——从概念到实践》
1. 线程的概念
在Linux中,线程 (Thread) 程序执行过程中最小的调度单位。线程是操作系统能够进行运算调度的基本单位,而进程是资源分配的基本单位。线程在同一个进程内共享资源(如内存、文件描述符等),但它们各自拥有独立的执行路径和程序计数器(PC)。线程的概念使得程序能够并发执行,提高效率。
1.2 为什么要使用线程?
在计算机世界里,程序的运行效率始终是开发者追求的核心目标之一。传统的单进程模型如同 “单线程工人”,一次只能处理一项任务,在面对复杂业务或高并发场景时显得力不从心。而 线程(Thread) 的出现,如同为程序配备了 “多线程工人团队”—— 它们共享资源却又能独立执行,让程序在效率与灵活性上实现了质的飞跃。
1.3 线程 VS 进程
维度 | 进程 | 线程 |
---|---|---|
资源分配单位 | 是(拥有独立内存、文件句柄等) | 否(共享进程资源) |
调度单位 | 是(但开销大,现代系统更倾向调度线程) | 是(操作系统直接调度的最小单位) |
创建开销 | 高(需分配内存、初始化资源) | 低(仅需创建线程栈和上下文) |
通信复杂度 | 高(需通过 IPC 机制,如管道、消息队列) | 低(可直接共享内存,无需额外机制) |
影响范围 | 进程崩溃不影响其他进程 | 线程崩溃可能导致整个进程崩溃 |
应用 | 服务器守护进程、需要强隔离的场景 | 浏览器多标签页、即时通信软件、后端服务的线程池 |
1.4 Linux 如何实现线程:轻量级进程(LWP)的奥秘
LWP(LightweightProcess,轻量级进程)是一种在操作系统中实现线程的方式。它是介于传统进程(Process)和线程(Thread)之间的一种抽象概念, 用于表示操作系统中的一个调度单位。
LWP的特点:
- 共享资源:多个LWP通常属于同一个进程,它们共享进程的资源,如内存空间、文件描述符等,但每个LWP拥有独立的程序计数器、堆栈和局部变量。这意味着,LWP之间共享大部分资源,但仍可以独立执行。
- 调度单位:LWP是操作系统调度的基本单位,虽然LWP之间共享进程的资源,但操作系统会根据各个LWP的状态进行独立调度。LWP能够在多核系统上并行执行,因此也称为轻量级线程。
- 对比线程和进程:进程是一个更重的实体,它有独立的内存空间和资源,且进程切换开销较大。线程(或LWP)是轻量级的调度单位,它们共享进程的资源,但每个线程都有自己的堆栈和执行上下文。
2.创建一个线程的完整流程
POSIX:pthread线程
创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void*(*start_routine)(void *), void *arg);
返回值:成功时返回 0,失败时返回错误码(如 EAGAIN 表示资源不足,EINVAL 表示参数无效)。
参数 | 作用 |
---|---|
pthread_t *thread | 输出参数,用于存储新创建线程的 ID(类型为 pthread_t)。 |
const pthread_attr_t *attr | 设置线程的属性(如栈大小、调度策略、分离状态等)。若为 NULL,则使用默认属性。 |
void* (start_routine)(void) | 线程的入口函数,新线程启动后将执行该函数。 |
void *arg | 传递给线程入口函数的参数(类型为 void*),支持任意类型的指针。 |
其他函数:
函数 | 作用 | 参数 |
---|---|---|
void pthread_exit(void* retval); | 主动退出当前线程(线程自己调用,结束执行) | retval : 线程的 “返回值”,类型是 void*,可传递任意数据(如字符串、结构体指针等)。若不需要返回值,填 NULL 即可。 |
int pthread_join(pthread_t thread, void **retval); | 阻塞等待指定线程结束(类似 “等人完成工作”) | thread:要等待的线程的 ID(由 pthread_create 输出)。retval: 传入一个 void* 指针的地址(即 void** 类型),用于存储线程的返回值。若不需要返回值,填 NULL 即可。 |
int pthread_detach(pthread_t thread); | 分离线程。 | pthread_tthread:要分离的线程ID,使其资源在终止时自动回收。 |
pthread_t pthread_self(void); | 获取当前线程的 ID | 无参数,返回调用该函数的线程的 ID(pthread_t 类型),用于标识当前线程。 |
#include <pthread.h>
#include <stdio.h>void* thread_func(void* arg) {printf("Hello from thread! Argument: %s\n", (char*)arg);return NULL;
}int main() {pthread_t tid;char* msg = "World";// 创建线程,入口函数为thread_func,参数为msgint ret = pthread_create(&tid, NULL, thread_func, msg);if (ret != 0) {perror("pthread_create failed");return 1;}// 等待线程结束pthread_join(tid, NULL);return 0;
}
C++11 thread线程
使用方式很简单,这里不过多介绍
#include <iostream>
#include <thread>// 普通函数作为线程执行体
void threadFunction() {std::cout << "线程ID: " << std::this_thread::get_id() << std::endl;
}int main() {std::thread t(threadFunction); // 创建并启动线程t.join(); // 等待线程结束return 0;
}
参照:cplusplus网站
组件 | 作用 |
---|---|
std::thread | 线程对象,管理线程创建与生命周期 |
std::mutex | 互斥锁,保护共享资源 |
std::lock_guard | 自动管理互斥锁(构造加锁,析构解锁) |
std::unique_lock | 灵活的锁管理(支持手动解锁、延迟加锁) |
std::condition_variable | 线程间 “等待 - 通知” 通信 |
std::atomic | 原子类型,提供无锁的原子操作 |
join() / detach() | 线程同步与分离 |
两者区别
维度 | Linux 线程(pthread) | C++ 线程(std::thread) |
---|---|---|
本质 | 操作系统提供的底层线程接口(C 语言风格) | C++ 标准库的跨平台线程抽象(面向对象风格) |
跨平台性 | 仅支持 POSIX 兼容系统(如 Linux、macOS) | 支持所有 C++11 及以上标准的平台(Linux、Windows、macOS 等) |
使用方式 | 基于函数和结构体(如 pthread_t、pthread_mutex_t),需手动管理资源(如检查返回值、释放锁) | 基于类和对象(如 std::thread、std::mutex),RAII 机制自动管理资源(如析构函数释放锁) |
错误处理 | 通过函数返回值(如 0 表示成功,非 0 表示错误码) | 通常通过抛出异常(如 std::system_error) |
语言依赖 | 依赖 C 语言接口,可在 C/C++ 中使用 | 仅在 C++ 中可用,依赖 C++ 特性(如 lambda、模板) |
3 多线程编程 同步与并发
如果两个或多个线程并发进行,同时访问和修改共享资源时,由于执行顺序的不确定性导致结果不可预测。
两个线程同时对全局变量 counter 执行 ++ 操作:
int count = 0;void* increment(void* arg) {for (int i = 0; i < 10000; i++) {count++; // 非原子操作(读取→修改→写入),可能导致结果错误}return NULL;
}
CPU 对count++的执行分为 “读 - 改 - 写” 三步,若两个线程的这三步交叉执行,会导致结果小于预期(如两个线程各执行 1 万次,最终counter可能小于 2 万)。
3.1 互斥锁
Mutex(互锁,MutualExclusion)是一种关键的同步机制,用于阻止许多个线程相同访问共享资源时,而避免了状态条(Race Condition)和数据不一致的问题。
函数 | 作用 |
---|---|
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutex_attr); | 初始化互斥锁 |
pthread_mutex_destroy(pthread_mutex_t *mutex) | 销毁互斥锁 |
pthread_mutex _lock(pthread_mutex_t *mutex) | 加锁(如果已被锁,则阻塞) |
pthread_mutex_trylock(pthread_mutex_t *mutex) | 尝试加锁(不会阻塞) |
pthread_mutex_unlock(pthread_mutex_t *mutex) | 解锁 |
其中,在加锁过程中,pthread_mutex_lock()函数和 pthread_mutex_trylock()函数的过程略有不同:
1.当使用pthread_mutex_lock()函数进行加锁时,若此时已经被锁,则尝试加锁的线程会被阻塞,直到互斥锁被其他线程释放,当pthread_mutex_lock0函数有返回值时,说明加锁成功;
2. 而使用pthread_mutex_trylock()函数进行加锁时,若此时已经被锁,则会返回EBUSY的错误码。
1. 创建锁
pthread_mutex_t mutex; // 定义互斥锁// 方法一:静态初始化(使用默认属性)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 方法二:动态初始化(可设置属性)
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); // NULL 表示使用默认属性2. 上锁
// 阻塞式加锁:若锁被占用,则线程阻塞等待
pthread_mutex_lock(&mutex);// 非阻塞式加锁:立即返回,成功返回0,失败返回EBUSY
int ret = pthread_mutex_trylock(&mutex);
if (ret == 0) {// 加锁成功
} else if (ret == EBUSY) {// 锁被占用,处理冲突
}3. 解锁
pthread_mutex_unlock(&mutex); // 释放锁,唤醒等待线程4. 销毁锁
pthread_mutex_destroy(&mutex); // 释放互斥锁资源
给刚才的代码加入互斥锁
pthread_mutex_t mutex;
int counter = 0;void* increment(void* arg) {for (int i = 0; i < 10000; i++) {pthread_mutex_lock(&mutex); // 加锁counter++; // 临界区pthread_mutex_unlock(&mutex); // 解锁}return NULL;
}
在c++11中引入了更方便的mutex,想要更多了解可看:
《C++11 多线程必学:std::mutex 详解与实战案例》
3.2 条件变量
在多线程场景中,线程常需等待特定条件(如数据就绪、资源释放)才能继续执行。若用轮询检查实现,会导致 CPU 空转,浪费系统资源。条件变量则提供了更高效的方案
条件变量需搭配互斥锁来使用:
条件变量用于等待 / 通知 “某个条件成立”,而条件的判断依赖共享变量(如生产者 - 消费者模型里的 “队列是否非空”)。这些共享变量属于临界资源,若不通过互斥锁保护,可能出现:
- 线程 A 判断 “条件不满足” 准备等待,此时线程 B 修改条件并通知,但线程 A 已错过通知,导致永久阻塞。
- 多个线程同时修改条件,引发数据竞争(如队列长度被并发修改)。
函数 | 作用 |
---|---|
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); | 初始化条件变量,为后续线程同步操作做准备。 |
int pthread_cond_destroy(pthread_cond_t *cond); | 销毁条件变量,释放相关资源,通常在不再需要条件变量时调用。 |
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); | 让线程阻塞等待条件满足,需配合互斥锁(pthread_mutex_t )使用,会原子性解锁互斥锁并等待,条件满足时重新加锁并返回。 |
int pthread_cond_signal(pthread_cond_t *cond); | 唤醒一个因该条件变量等待的线程,若有多个等待线程,按调度策略选一个唤醒。 |
pthread_cond_broadcast(pthread_cond_t *cond) | 唤醒所有因该条件变量等待的线程,常用于需所有等待线程响应的场景 。 |
示例代码(封装cond类)
#pragma once#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"
//通过 C++ 类封装 POSIX 条件变量 API,简化条件变量的使用,实现线程间的等待 / 通知机制
//条件变量在构造函数中初始化(pthread_cond_init),在析构函数中销毁(pthread_cond_destroy),符合 RAII 原则,避免资源泄漏。
namespace CondModule
{using namespace LockModule;class Cond{public:Cond()//初始化条件变量_cond。{int n = ::pthread_cond_init(&_cond, nullptr);(void)n;}//Wait() 函数接收 Mutex 对象引用,通过 mutex.LockPtr() 获取底层 pthread_mutex_t*,确保条件变量与互斥锁正确配合使用。void Wait(Mutex &mutex){int n = ::pthread_cond_wait(&_cond, mutex.LockPtr());}void Notify()//唤醒一个等待该条件变量的线程。{int n = ::pthread_cond_signal(&_cond);(void)n;}void NotifyAll()//唤醒所有等待该条件变量的线程。{int n = ::pthread_cond_broadcast(&_cond);(void)n;}~Cond(){int n = ::pthread_cond_destroy(&_cond);}private:pthread_cond_t _cond;};
}
在c++11中引入了更方便的条件变量condition_variable,想要更多了解可看:
《Linux 环境下 C++ 条件变量与 POSIX 条件变量详解》
3.3 读写锁
读写锁(Read - Write Lock)是一种用于解决并发访问共享资源时同步问题的同步机制,尤其适用于读操作远多于写操作的场景,能有效提升并发性能。
3.3.1 核心概念与状态
读写锁把对共享资源的访问者分为读者(Reader)和写者(Writer):
- 读锁(共享锁):允许多个线程同时持有,用于纯读操作(不修改资源),因为读操作本身不影响数据一致性,并发读可提升效率。
- 写锁(排他锁 / 独占锁):同一时刻仅允许一个线程持有,用于写操作(修改资源),保证写操作的原子性,避免多线程写导致数据混乱。
- 读写锁有三种状态:读模式加锁、写模式加锁、不加锁 。
3.3.2 加锁规则(核心逻辑)
写锁优先时(常见默认行为,也可配置公平策略):
- 若读写锁处于写加锁状态:所有后续尝试加读锁或写锁的线程都会被阻塞,直到写锁释放。保证写操作的原子性,避免写过程中被其他线程干扰。
- 若读写锁处于读加锁状态:
新的读锁请求可直接成功(多线程共享读锁);
新的写锁请求会被阻塞,直到所有读锁都释放(避免读 - 写冲突,保证写操作能修改 “干净” 的数据 )。 - 特殊情况:若读锁已持有,新写锁请求到来后,后续读锁请求可能被 “限流”(部分实现会让新读锁等待,防止写请求长期阻塞,即 “写优先” 或 “读写公平” 策略 )。
公平策略下:严格按照线程请求顺序分配锁,写锁请求和读锁请求排队,避免某类操作(如读)长期占优导致另一类操作(如写)饥饿。
3.3.3 实例
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>// 读写锁、互斥锁、共享数据
pthread_rwlock_t rwlock;
int shared_data = 0;// 读操作线程函数
void* read_task(void* arg) {while (1) {// 获取读锁pthread_rwlock_rdlock(&rwlock);printf("Read: shared_data = %d\n", shared_data);// 释放读锁pthread_rwlock_unlock(&rwlock);sleep(1);}return NULL;
}// 写操作线程函数
void* write_task(void* arg) {int count = 0;while (1) {// 获取写锁pthread_rwlock_wrlock(&rwlock);shared_data = ++count;printf("Write: shared_data updated to %d\n", shared_data);// 释放写锁pthread_rwlock_unlock(&rwlock);sleep(3);}return NULL;
}int main() {// 初始化读写锁pthread_rwlock_init(&rwlock, NULL);pthread_t read_thread1, read_thread2, write_thread;// 创建读线程和写线程pthread_create(&read_thread1, NULL, read_task, NULL);pthread_create(&read_thread2, NULL, read_task, NULL);pthread_create(&write_thread, NULL, write_task, NULL);// 等待线程结束(实际需更优雅退出)pthread_join(read_thread1, NULL);pthread_join(read_thread2, NULL);pthread_join(write_thread, NULL);// 销毁读写锁pthread_rwlock_destroy(&rwlock);return 0;
}
4. 实战
4.1 封装原Thread接口(区别于C++ 标准库的线程)
简化原生pthread接口的使用,提供更面向对象、更安全的线程操作方式。
#ifndef _THREAD_HPP__
#define _THREAD_HPP__#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include <sys/types.h>
#include <unistd.h>// 简化原生pthread接口的使用,提供更面向对象、更安全的线程操作方式。
// 通过std::function支持任意可调用对象作为线程函数,比原生pthread只能使用静态函数更灵活。// v1
namespace ThreadModule
{// 标准化线程执行的函数签名(接受字符串参数,无返回值)using func_t = std::function<void(std::string name)>;// 为每个线程生成唯一ID的计数器static int number = 1;// 定义线程生命周期状态enum class TSTATUS{NEW, // 线程已创建但未运行RUNNING, // 线程正在运行STOP // 线程已停止};class Thread{private:// 成员方法!// 线程执行的静态函数,作为pthread_create的入口static void *Routine(void *args){// 将void*参数转换回Thread对象Thread *t = static_cast<Thread *>(args);// 设置线程状态为运行中t->_status = TSTATUS::RUNNING;// 执行用户传入的函数对象,将线程名称作为参数传入t->_func(t->Name());return nullptr;}// 启用线程分离,将_joinable标记为falsevoid EnableDetach() { _joinable = false; }public:// 构造函数,接受一个可调用对象作为线程执行的任务Thread(func_t func) : _func(func), _status(TSTATUS::NEW), _joinable(true){// 为线程生成唯一的名称_name = "Thread-" + std::to_string(number++);// 获取当前进程的ID_pid = getpid();}// 启动线程bool Start(){// 只有当线程状态不是RUNNING时才能启动if (_status != TSTATUS::RUNNING){// 创建线程,将当前Thread对象作为参数传递给Routine函数int n = ::pthread_create(&_tid, nullptr, Routine, this);// 如果创建失败,返回falseif (n != 0)return false;return true;}return false;}// 停止线程bool Stop(){// 只有当线程状态为RUNNING时才能停止if (_status == TSTATUS::RUNNING){// 取消线程int n = ::pthread_cancel(_tid);// 如果取消失败,返回falseif (n != 0)return false;// 设置线程状态为停止_status = TSTATUS::STOP;return true;}return false;}// 等待线程结束bool Join(){// 只有当线程是可连接状态时才能调用joinif (_joinable){// 等待线程结束int n = ::pthread_join(_tid, nullptr);// 如果等待失败,返回falseif (n != 0)return false;// 设置线程状态为停止_status = TSTATUS::STOP;return true;}return false;}// 分离线程void Detach(){// 启用线程分离EnableDetach();// 分离线程pthread_detach(_tid);}// 判断线程是否是可连接状态bool IsJoinable() { return _joinable; }// 获取线程的名称std::string Name() { return _name; }// 析构函数~Thread(){}private:std::string _name; // 线程的名称pthread_t _tid; // 线程的IDpid_t _pid; // 进程的IDbool _joinable; // 是否是分离的,默认不是,标记线程是否为“可连接”(joinable)状态。func_t _func; // 存储线程实际执行的函数(任务)。TSTATUS _status; // 记录线程的当前状态。};
}#endif
4.2 线程池
《深入剖析 C++ 线程池实现》
注意:使用的都是C++ 标准库的线程,互斥锁,条件变量,建议先了解!
4.3 力扣题目
测试题目 按序打印, H2O 生成
如有错误,欢迎指正!