C++ 线程库

目录

一、基础核心

[1. 线程库的底层逻辑:C++11 vs Linux pthread](#1. 线程库的底层逻辑:C++11 vs Linux pthread)

[2. 第一个线程程序](#2. 第一个线程程序)

[3. 核心概念:join vs detach](#3. 核心概念:join vs detach)

[4. 线程同步基础:解决数据竞争](#4. 线程同步基础:解决数据竞争)

[1. std::mutex:互斥锁(最常用)](#1. std::mutex:互斥锁(最常用))

[2. std::condition_variable:条件变量(线程间通信)](#2. std::condition_variable:条件变量(线程间通信))

[3. std::atomic:原子操作(无锁同步)](#3. std::atomic:原子操作(无锁同步))

二、补充知识点

[1. C++11 线程库与 pthread 的区别?](#1. C++11 线程库与 pthread 的区别?)

[2. 死锁的原因及避免方法?](#2. 死锁的原因及避免方法?)

[3. 条件变量的虚假唤醒是什么?如何避免?](#3. 条件变量的虚假唤醒是什么?如何避免?)

[4. std::lock_guard vs std::unique_lock?](#4. std::lock_guard vs std::unique_lock?)

[5. 原子操作的实现原理?为什么比互斥锁快?](#5. 原子操作的实现原理?为什么比互斥锁快?)

[6. 线程局部存储(TLS)的实现与应用?](#6. 线程局部存储(TLS)的实现与应用?)

[7. 易错点](#7. 易错点)

三、线程库的实践

[1. 实战 1:工业级线程池实现(muduo 风格)](#1. 实战 1:工业级线程池实现(muduo 风格))

[2. 实战 2:高性能同步:读写锁(std::shared_mutex)](#2. 实战 2:高性能同步:读写锁(std::shared_mutex))

[3. 实战 3:Linux 线程优化:亲和性 + 栈大小](#3. 实战 3:Linux 线程优化:亲和性 + 栈大小)

[4. 调试技巧](#4. 调试技巧)

[1. 查看线程信息:pstack/ps](#1. 查看线程信息:pstack/ps)

[2. GDB 调试多线程](#2. GDB 调试多线程)

[3. 检测数据竞争:tsan](#3. 检测数据竞争:tsan)

四、避坑指南

五、总结


在 Linux C++ 后端开发中,线程库 是实现高并发、高性能服务的核心工具 ------C++11 引入的std::thread系列接口封装了 Linux 底层的pthread库,既保留了系统级的高效性,又提供了现代 C++ 的类型安全和易用性。

一、基础核心

1. 线程库的底层逻辑:C++11 vs Linux pthread

Linux 系统层面的线程由pthread(POSIX 线程)库实现,而 C++11 的<thread>头文件是对pthread类型安全封装------ 二者的核心对应关系如下:

C++11 线程库组件 底层 Linux pthread 接口 核心作用
std::thread pthread_create/pthread_join/pthread_detach 线程创建、启动、销毁
std::mutex pthread_mutex_t/pthread_mutex_lock/unlock 互斥锁,保护临界区
std::condition_variable pthread_cond_t/pthread_cond_wait/signal 线程间条件等待 / 通知
std::atomic atomic内置函数(x86:lock指令) 无锁原子操作
std::thread_local __thread(GCC 扩展) 线程局部存储(TLS)

核心认知 :C++11 线程库是 "跨平台封装",在 Linux 下最终会调用pthread系统调用,因此编译时需链接-lpthread库。

2. 第一个线程程序

核心语法:std::thread的基础用法

cpp 复制代码
#include <iostream>
#include <thread> // C++11线程库头文件
#include <unistd.h> // Linux sleep函数

// 线程函数:普通函数
void thread_func(int num, const std::string& msg) {
    for (int i = 0; i < 3; ++i) {
        // 输出线程ID + 传入参数
        std::cout << "Thread " << num << " msg: " << msg 
                  << " (tid: " << std::this_thread::get_id() << ")" << std::endl;
        sleep(1); // 模拟耗时操作
    }
}

int main() {
    // 1. 创建线程:传入函数+参数
    std::thread t1(thread_func, 1, "Hello Linux");
    std::thread t2(thread_func, 2, "C++ Thread");

    // 2. 等待线程结束(join):主线程阻塞,直到子线程完成
    t1.join();
    t2.join();

    // 3. 主线程继续执行
    std::cout << "Main thread (tid: " << std::this_thread::get_id() << ") exit" << std::endl;
    return 0;
}

编译运行(Linux)

bash 复制代码
# 必须链接pthread库,-std=c++11及以上
g++ -std=c++11 thread_basic.cpp -o thread_basic -lpthread
./thread_basic

输出示例

bash 复制代码
Thread 1 msg: Hello Linux (tid: 140709260797696)
Thread 2 msg: C++ Thread (tid: 140709252404992)
Thread 1 msg: Hello Linux (tid: 140709260797696)
Thread 2 msg: C++ Thread (tid: 140709252404992)
Thread 1 msg: Hello Linux (tid: 140709260797696)
Thread 2 msg: C++ Thread (tid: 140709252404992)
Main thread (tid: 140709268926336) exit
3. 核心概念:join vs detach

std::thread创建后必须调用join()detach(),否则主线程结束时会触发std::terminate()(程序 abort),二者的核心区别如下:

操作 核心语义 线程归属 风险点 适用场景
join() 主线程阻塞,等待子线程完成 归主线程管理 主线程阻塞过久,影响响应 需等待子线程结果、资源回收
detach() 子线程与主线程分离,后台运行 归系统管理(守护线程) 子线程访问的变量可能悬空 后台日志、监控等无需等待的任务

错误示例:未调用join/detach

cpp 复制代码
#include <iostream>
#include <thread>

void func() { sleep(1); }

int main() {
    std::thread t(func);
    // 未调用t.join()或t.detach()
    return 0; // 程序abort,输出:terminate called without an active exception
}

正确示例:detach的安全用法

cpp 复制代码
#include <iostream>
#include <thread>
#include <unistd.h>

int main() {
    // 捕获全局/静态变量,避免局部变量悬空
    std::thread t([]() {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Detached thread running..." << std::endl;
            sleep(1);
        }
    });
    t.detach(); // 分离线程

    std::cout << "Main thread exit" << std::endl;
    sleep(6); // 主线程等待足够久,确保子线程完成
    return 0;
}
4. 线程同步基础:解决数据竞争

多线程访问共享变量时会出现数据竞争(未定义行为),必须用同步原语保护临界区,小白先掌握 3 个核心同步工具:

1. std::mutex:互斥锁(最常用)

核心作用:保证同一时间只有一个线程进入临界区,示例:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <unistd.h>

int count = 0; // 共享变量
std::mutex mtx; // 全局互斥锁

void increment() {
    for (int i = 0; i < 10000; ++i) {
        // 加锁:RAII方式,自动解锁(避免死锁)
        std::lock_guard<std::mutex> lock(mtx);
        count++; // 临界区:仅一个线程能执行
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();
    std::cout << "count = " << count << std::endl; // 正确输出20000
    return 0;
}
2. std::condition_variable:条件变量(线程间通信)

核心作用:让线程等待某个条件满足,避免忙等(CPU 空转),示例:生产者消费者模型(简化版):

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <unistd.h>

std::queue<int> q; // 共享队列
std::mutex mtx;
std::condition_variable cv;

// 生产者:生产数据
void producer() {
    for (int i = 1; i <= 5; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        q.push(i);
        std::cout << "Produced: " << i << std::endl;
        cv.notify_one(); // 通知消费者
        sleep(1);
    }
}

// 消费者:消费数据
void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        // 等待条件:队列非空(避免虚假唤醒)
        cv.wait(lock, []() { return !q.empty(); });
        
        int val = q.front();
        q.pop();
        std::cout << "Consumed: " << val << std::endl;
        lock.unlock(); // 提前解锁,减少阻塞

        if (val == 5) break; // 消费完退出
    }
}

int main() {
    std::thread t_prod(producer);
    std::thread t_cons(consumer);

    t_prod.join();
    t_cons.join();
    return 0;
}
3. std::atomic:原子操作(无锁同步)

核心作用:对简单类型(int/long/ 指针)的操作是原子的,无需加锁,性能更高,示例:

cpp 复制代码
#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> count = 0; // 原子变量

void increment() {
    for (int i = 0; i < 1000000; ++i) {
        count++; // 原子操作,无数据竞争
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();
    std::cout << "atomic count = " << count << std::endl; // 正确输出2000000
    return 0;
}

二、补充知识点

1. C++11 线程库与 pthread 的区别?
  • 封装层级std::thread是 C++ 标准库,封装了 Linux 的pthread系统调用,跨平台(Windows/Linux);pthread是 Linux 专属的 C 语言接口,无跨平台性;
  • 类型安全std::thread支持任意可调用对象(Lambda / 仿函数 / 成员函数),编译期检查参数类型;pthread仅支持 C 函数指针,类型检查弱;
  • 异常处理std::thread抛出 C++ 异常,可捕获;pthread通过返回码传递错误,需手动检查;
  • 编译链接std::thread仍需链接-lpthread(Linux 下),本质依赖pthread
2. 死锁的原因及避免方法?
  • 死锁原因 :四个条件同时满足:
    1. 互斥:资源只能被一个线程持有;
    2. 持有并等待:线程持有资源 A,等待资源 B;
    3. 不可剥夺:资源不能被强制收回;
    4. 循环等待:线程 1 等线程 2 的资源,线程 2 等线程 1 的资源。
  • 避免方法
    1. 按固定顺序加锁(如先锁 mtx1,再锁 mtx2);
    2. 使用std::lock一次性加多个锁;
    3. 使用std::timed_mutex设置超时,避免永久等待;
    4. 最小化锁的持有时间。
3. 条件变量的虚假唤醒是什么?如何避免?

虚假唤醒 :线程被notify_one()唤醒后,条件可能已不满足(如多个消费者被唤醒,只有一个能拿到数据);

避免方法

  • cv.wait(lock, 条件判断),唤醒后重新检查条件;
  • 循环检查条件:
cpp 复制代码
while (q.empty()) { cv.wait(lock); }
4. std::lock_guard vs std::unique_lock
特性 std::lock_guard std::unique_lock
锁管理 RAII,构造加锁,析构解锁 RAII,支持手动加锁 / 解锁
灵活性 低(不可手动解锁) 高(lock()/unlock()
性能 无额外开销 轻微额外开销(状态管理)
适用场景 简单临界区,全程加锁 条件变量、提前解锁、超时锁
配合条件变量 不支持 必须用(cv.wait()需要解锁)
5. 原子操作的实现原理?为什么比互斥锁快?
  • 原理 :原子操作通过 CPU 的原子指令 (如 x86 的lock前缀)实现,保证指令执行期间总线锁定,其他 CPU 核心无法访问该内存;
  • 比互斥锁快的原因
    1. 互斥锁涉及内核态 / 用户态切换(上下文开销);
    2. 原子操作是用户态指令,无上下文切换,仅锁定总线(纳秒级开销);
    3. 原子操作无阻塞(自旋),互斥锁可能导致线程挂起。
6. 线程局部存储(TLS)的实现与应用?
  • 实现 :Linux 下 C++11 的std::thread_local封装了__thread(GCC 扩展),每个线程有独立的变量副本,存储在线程私有栈 / 数据区;
  • 应用场景
    1. 线程独立的计数器、日志 ID;
    2. 避免函数静态变量的线程安全问题;
    3. 数据库连接池的线程私有连接。

示例:thread_local的用法

cpp 复制代码
#include <iostream>
#include <thread>
#include <unistd.h>

thread_local int tls_num = 0; // 线程局部变量

void func(int id) {
    tls_num = id; // 每个线程修改自己的副本
    for (int i = 0; i < 3; ++i) {
        std::cout << "Thread " << id << " tls_num: " << tls_num << std::endl;
        sleep(1);
    }
}

int main() {
    std::thread t1(func, 1);
    std::thread t2(func, 2);

    t1.join();
    t2.join();
    return 0;
}
7. 易错点
  • 误区 1std::mutex可以递归加锁 → 错!std::mutex是非递归锁,重复加锁会死锁,需用std::recursive_mutex(工业级慎用,易隐藏逻辑问题);
  • 误区 2:原子操作适用于所有场景 → 错!原子操作仅支持简单类型的简单操作(++/--/ 赋值),复杂逻辑仍需互斥锁;
  • 误区 3detach后线程可访问局部变量 → 错!detach的线程若访问局部变量,变量销毁后会悬空(未定义行为);
  • 误区 4 :条件变量的notify_one一定会唤醒线程 → 错!若notify_onewait前调用,线程会永久等待(需初始化条件);
  • 误区 5:多线程一定比单线程快 → 错!线程切换有开销(约 1-10μs),高频小任务的多线程可能比单线程慢(需用线程池优化)。

三、线程库的实践

1. 实战 1:工业级线程池实现(muduo 风格)

线程池是 Linux 高并发服务的核心组件,解决 "频繁创建销毁线程" 的开销问题,核心设计要点:

  • 任务队列 + 工作线程池;
  • 优雅关闭(处理剩余任务);
  • 移动语义优化任务拷贝;
  • 避免数据竞争。

完整代码

cpp 复制代码
#include <iostream>
#include <functional>
#include <queue>
#include <vector>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <future>
#include <utility>

// 任务类型:包装任意可调用对象,支持返回值
using Task = std::function<void()>;

class ThreadPool {
private:
    std::vector<std::thread> workers_; // 工作线程
    std::queue<Task> tasks_; // 任务队列
    std::mutex mtx_; // 保护任务队列
    std::condition_variable cv_; // 任务通知
    std::atomic<bool> running_; // 线程池运行状态
    size_t max_workers_; // 最大线程数

public:
    // 构造函数:初始化线程池
    explicit ThreadPool(size_t max_workers = std::thread::hardware_concurrency())
        : running_(true), max_workers_(max_workers) {
        // 创建工作线程
        for (size_t i = 0; i < max_workers_; ++i) {
            workers_.emplace_back([this]() {
                while (running_) {
                    Task task;
                    // 加锁取任务
                    {
                        std::unique_lock<std::mutex> lock(mtx_);
                        // 等待任务或停止信号
                        cv_.wait(lock, [this]() {
                            return !running_ || !tasks_.empty();
                        });
                        // 停止且无任务,退出线程
                        if (!running_ && tasks_.empty()) return;
                        // 取任务(移动语义,避免拷贝)
                        task = std::move(tasks_.front());
                        tasks_.pop();
                    }
                    // 执行任务(解锁后执行,减少阻塞)
                    task();
                }
            });
        }
    }

    // 析构函数:优雅关闭线程池
    ~ThreadPool() {
        running_ = false;
        cv_.notify_all(); // 唤醒所有线程
        // 等待所有线程结束
        for (auto& worker : workers_) {
            if (worker.joinable()) {
                worker.join();
            }
        }
    }

    // 禁用拷贝和移动
    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;
    ThreadPool(ThreadPool&&) = delete;
    ThreadPool& operator=(ThreadPool&&) = delete;

    // 添加任务:支持任意参数,返回future获取结果
    template <typename F, typename... Args>
    auto add_task(F&& f, Args&&... args) -> std::future<decltype(f(args...))> {
        // 包装任务为std::packaged_task,支持返回值
        using ReturnType = decltype(f(args...));
        auto task = std::make_shared<std::packaged_task<ReturnType()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );
        // 获取future,供调用方获取结果
        std::future<ReturnType> fut = task->get_future();

        // 加锁添加任务
        {
            std::lock_guard<std::mutex> lock(mtx_);
            if (!running_) {
                throw std::runtime_error("add task to stopped ThreadPool");
            }
            // 封装为Task,加入队列
            tasks_.emplace([task]() { (*task)(); });
        }
        cv_.notify_one(); // 唤醒一个线程执行任务
        return fut;
    }
};

// 测试:任务函数(带返回值)
int sum(int a, int b) {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    return a + b;
}

int main() {
    ThreadPool pool(4); // 4个工作线程

    // 添加10个任务,获取future
    std::vector<std::future<int>> results;
    for (int i = 0; i < 10; ++i) {
        results.emplace_back(pool.add_task(sum, i, i*2));
    }

    // 输出任务结果
    for (auto& fut : results) {
        std::cout << "Result: " << fut.get() << std::endl;
    }

    return 0;
}

编译运行(Linux)

cpp 复制代码
g++ -std=c++17 thread_pool.cpp -o thread_pool -lpthread
./thread_pool

亮点

  1. 类型安全 :支持任意返回值的任务,通过std::future获取结果;
  2. 性能优化:移动语义减少任务拷贝,解锁后执行任务减少阻塞;
  3. 优雅关闭:析构时处理剩余任务,避免资源泄漏;
  4. 异常安全:禁止拷贝 / 移动,避免线程池被非法复制。
2. 实战 2:高性能同步:读写锁(std::shared_mutex

Linux 下读多写少的场景(如配置读取、缓存),用std::shared_mutex(C++17)实现读写锁,读并发、写互斥,性能远超普通互斥锁:

cpp 复制代码
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
#include <unistd.h>

std::shared_mutex rw_mtx; // 读写锁
int config = 10; // 共享配置

// 读线程:共享加锁,允许多个读
void read_config(int id) {
    for (int i = 0; i < 3; ++i) {
        std::shared_lock<std::shared_mutex> lock(rw_mtx);
        std::cout << "Reader " << id << " read config: " << config << std::endl;
        lock.unlock();
        sleep(1);
    }
}

// 写线程:独占加锁,仅一个写
void write_config(int new_val) {
    std::unique_lock<std::shared_mutex> lock(rw_mtx);
    std::cout << "Writer update config to: " << new_val << std::endl;
    config = new_val;
    sleep(2);
}

int main() {
    // 5个读线程,1个写线程
    std::vector<std::thread> readers;
    for (int i = 1; i <= 5; ++i) {
        readers.emplace_back(read_config, i);
    }
    std::thread writer(write_config, 20);

    // 等待所有线程
    for (auto& t : readers) { t.join(); }
    writer.join();
    return 0;
}
3. 实战 3:Linux 线程优化:亲和性 + 栈大小

工业级场景需优化线程性能,Linux 下可调整线程的核心亲和性(绑定 CPU)和栈大小:

cpp 复制代码
#include <iostream>
#include <thread>
#include <pthread.h> // Linux pthread接口

// 设置线程亲和性:绑定到指定CPU核心
void set_thread_affinity(std::thread& t, int cpu_core) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(cpu_core, &cpuset);
    pthread_t tid = t.native_handle(); // 获取pthread_t
    pthread_setaffinity_np(tid, sizeof(cpu_set_t), &cpuset);
}

// 设置线程栈大小
void set_thread_stack_size(std::thread& t, size_t stack_size) {
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setstacksize(&attr, stack_size);
    // 需在创建线程前设置,此处仅示例
    pthread_attr_destroy(&attr);
}

void func() {
    while (true) { sleep(1); }
}

int main() {
    std::thread t(func);
    // 绑定线程到CPU 0核心(Linux下核心编号从0开始)
    set_thread_affinity(t, 0);
    std::cout << "Thread bound to CPU 0" << std::endl;

    t.join();
    return 0;
}
4. 调试技巧
1. 查看线程信息:pstack/ps
bash 复制代码
# 查看进程的所有线程
ps -T -p <pid>
# 查看线程调用栈
pstack <pid>
2. GDB 调试多线程
bash 复制代码
gdb ./thread_pool
(gdb) info threads # 查看所有线程
(gdb) thread <id> # 切换到指定线程
(gdb) bt # 查看线程调用栈
(gdb) set scheduler-locking on # 锁定线程调度,避免切换
3. 检测数据竞争:tsan
bash 复制代码
# 编译时启用tsan,检测数据竞争
g++ -std=c++17 thread.cpp -o thread -lpthread -fsanitize=thread
./thread # 输出数据竞争的详细信息

四、避坑指南

  • 坑点 1 :线程捕获局部变量的引用(detach后悬空)解决方案 :值捕获变量,或用std::shared_ptr管理资源;
  • 坑点 2 :互斥锁未解锁(异常导致)解决方案 :始终用 RAII 锁(lock_guard/unique_lock),避免手动解锁;
  • 坑点 3 :条件变量未处理虚假唤醒解决方案 :用带谓词的wait,或循环检查条件;
  • 坑点 4 :线程池任务队列满(内存溢出)解决方案 :限制队列大小,add_task时阻塞或返回错误;
  • 坑点 5 :原子变量的复杂操作(如a += b)非原子解决方案 :用std::atomic::fetch_add等原子操作函数,或加锁。
  • 优先使用线程池:避免频繁创建销毁线程(Linux 下线程创建开销约 1ms);
  • 最小化锁粒度:仅在临界区加锁,提前解锁;
  • 读多写少用读写锁std::shared_mutexstd::mutex性能高 5-10 倍;
  • 简单操作用原子变量:避免互斥锁的上下文切换开销;
  • 线程数控制:CPU 密集型任务线程数 = CPU 核心数,IO 密集型任务线程数 = CPU 核心数 * 2;
  • 优雅关闭:线程池 / 服务退出时,处理剩余任务,释放资源。

五、总结

  • 基础层std::thread封装 Linux pthread,必须调用join/detach,编译链接-lpthread
  • 同步层 :互斥锁(mutex)保护临界区,条件变量(condition_variable)实现线程通信,原子操作(atomic)优化简单同步;
  • 实战层:线程池是工业级高并发的核心,读写锁优化读多写少场景,线程亲和性提升缓存命中率;
  • 避坑层:避免数据竞争、死锁、悬空引用,用 RAII 管理锁,处理虚假唤醒。
相关推荐
木土雨成小小测试员2 小时前
Python测试开发之后端一
开发语言·数据库·人工智能·python·django·sqlite
superman超哥2 小时前
Serialize 与 Deserialize Trait:Rust 类型系统与序列化的完美融合
开发语言·rust·开发工具·编程语言·rust序列化·rust类型·serialize
黎子越2 小时前
python循环相关联系
开发语言·python·算法
安然无虞2 小时前
「正则表达式」精讲
开发语言·测试工具·正则表达式
云深处@2 小时前
二叉搜索树
数据结构·c++
csbysj20202 小时前
DOM 解析器错误
开发语言
安全二次方security²2 小时前
CUDA C++编程指南(7.2)——C++语言扩展之变量内存空间指定符
c++·人工智能·nvidia·cuda·内存空间指定符·__shared__·__device__
近津薪荼2 小时前
优选算法——双指针1(数组分块)
c++·学习·算法
Дерек的学习记录2 小时前
二叉树(下)
c语言·开发语言·数据结构·学习·算法·链表