多线程

166 阅读9分钟

C++ 多线程知识体系

一、多线程基础概念

1. 线程与进程

  • 进程:操作系统资源分配的基本单位,拥有独立的地址空间
  • 线程:CPU调度的基本单位,共享进程的资源,有独立的栈和寄存器

2. 并发与并行

  • 并发:宏观上同时执行,微观上交替执行
  • 并行:真正的同时执行,需要多核CPU支持

3. 线程安全

  • 多个线程访问共享资源时不会出现数据不一致或不可预期的结果
  • 实现方式:互斥锁、原子操作、线程局部存储等

二、C++多线程支持

1. C++11标准引入的线程库

#include <thread>
std::thread t(func, args...); // 创建线程
t.join(); // 等待线程结束
t.detach(); // 分离线程

2. 线程管理

  • 线程ID:std::thread::id,可通过std::this_thread::get_id()获取
  • 硬件并发数:std::thread::hardware_concurrency()
  • 线程休眠:std::this_thread::sleep_for(), std::this_thread::sleep_until()

三、线程同步机制

1. 互斥量(Mutex)

#include <mutex>
std::mutex m;
m.lock();
// 临界区
m.unlock();

// 推荐使用RAII风格的锁
std::lock_guard<std::mutex> lock(m);
std::unique_lock<std::mutex> ulock(m, std::defer_lock);

2. 条件变量(Condition Variable)

#include <condition_variable>
std::condition_variable cv;
std::mutex m;
bool ready = false;

// 等待线程
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, []{return ready;});

// 通知线程
{
    std::lock_guard<std::mutex> lock(m);
    ready = true;
}
cv.notify_one(); // 或 notify_all()

3. 原子操作

#include <atomic>
std::atomic<int> counter(0);
counter.fetch_add(1); // 原子递增

4. 读写锁(C++17)

#include <shared_mutex>
std::shared_mutex sm;

// 写锁
{
    std::unique_lock<std::shared_mutex> lock(sm);
    // 写操作
}

// 读锁
{
    std::shared_lock<std::shared_mutex> lock(sm);
    // 读操作
}

四、高级线程管理

1. 线程池实现

class ThreadPool {
public:
    ThreadPool(size_t);
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) 
        -> std::future<typename std::result_of<F(Args...)>::type>;
    ~ThreadPool();
private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

2. 异步操作

#include <future>
// 异步执行
auto future = std::async(std::launch::async, func, args...);
auto result = future.get(); // 获取结果

// 承诺和未来值
std::promise<int> p;
auto f = p.get_future();
p.set_value(42); // 设置值

3. 线程局部存储

thread_local int thread_specific_value = 0;

五、常见多线程问题

1. 死锁

  • 产生条件:互斥、占有且等待、非抢占、循环等待
  • 解决方案:
    • 按固定顺序获取锁
    • 使用std::lock()同时锁定多个互斥量
    • 使用std::scoped_lock(C++17)

2. 竞态条件

  • 解决方案:正确使用同步机制保护共享数据

3. 虚假唤醒

  • 条件变量等待时应使用谓词检查

六、性能考虑

  1. 锁粒度:尽量减小临界区范围
  2. 锁争用:减少线程对锁的竞争
  3. 无锁编程:在适当场景使用原子操作
  4. 缓存友好性:注意伪共享(false sharing)问题

七、C++20新增特性

1. 信号量(Semaphore)

#include <semaphore>
std::counting_semaphore<10> sem(0);
sem.acquire(); // P操作
sem.release(); // V操作

2. 闩(Latch)和屏障(Barrier)

#include <latch>
std::latch completion_latch(10); // 等待10个线程
completion_latch.arrive_and_wait();

#include <barrier>
std::barrier sync_point(5); // 5个线程的同步点
sync_point.arrive_and_wait();

八、最佳实践

  1. 优先使用高级抽象(std::async, std::future等)
  2. 避免裸锁,使用RAII包装器
  3. 尽量减少共享数据
  4. 使用线程安全的数据结构
  5. 注意异常安全

九、调试与测试

  1. 使用线程分析工具(如Valgrind的Helgrind)
  2. 编写确定性测试
  3. 使用静态分析工具检测潜在问题

温故知新

一、join和detach区别
在 C++ 中,std::thread 是用来表示线程的标准库类。每个 std::thread 对象都与一个线程相关联。当线程结束时,有两种处理方式:join 和 detach。这两种方法处理线程生命周期的不同方式:
join

等待线程结束:调用 join() 方法会等待线程自然结束。直到线程执行完毕,调用 join() 的线程才会继续执行。
资源清理:join() 会确保线程的资源被适当地清理。一旦线程结束,std::thread 对象将不再与任何线程关联。
用途:当需要确保线程执行完毕后再继续执行其他任务时,使用 join() 是合适的。

detach

分离线程:调用 detach() 方法会将线程与 std::thread 对象分离。std::thread 对象将不再控制线程,线程将继续执行直到完成。
资源管理:一旦线程被分离,线程的资源(如堆栈)将由操作系统在线程结束时自动清理。但是,如果线程尝试访问其所属程序的全局或静态变量,这些变量可能已经被销毁,因为 std::thread 对象可能已经销毁了。
用途:当不需要等待线程结束,或者线程执行的是长时间运行的任务,而主线程需要继续执行其他任务时,使用 detach() 是合适的。                       

二、原子操作和内存顺序

std::atomic 是 C++11 引入的一个用于原子操作的模板类,主要用于多线程环境下进行无锁的线程安全操作。它能够确保对数据的读取和写入操作是原子的,避免了线程间的竞争条件和数据不一致的问题。

total_time += stats->total_time.load(std::memory_order_relaxed)
std::memory_order_relaxed 是用于指定原子操作的内存顺序
内存顺序控制着不同线程间操作的可见性和执行顺序。C++11 引入了原子操作和内存顺序的概念,以支持多线程并发编程中的同步问题

    s :这表示不做任何同步保证,操作仅仅是原子的。也就是说,这个操作不会影响其他操作的顺序,但它会确保原子操作本身是正确的。其他线程可能在看到这个操作的结果之前已经执行了其它操作,因此不适用于需要同步或排序的场景。
std::memory_order_acquire:表示当前操作之前的所有读写操作必须在当前操作完成后才能执行。这常用于获取锁或同步信息。
std::memory_order_release:表示当前操作之后的所有读写操作必须在当前操作之前执行。
std::memory_order_seq_cst:表示操作顺序遵循严格的顺序,确保在多线程环境下所有操作的执行顺序一致。

内存顺序模型对比表
内存顺序                    保证内容                                性能    典型使用场景
memory_order_relaxed    仅保证原子性,无顺序保证                最高    计数器、不需要同步的标志位
memory_order_acquire    保证后续操作不会被重排到它之前            较高    读取共享数据前的同步
memory_order_release    保证前面的操作不会被重排到它之后            较高    写入共享数据后的同步
memory_order_seq_cst    全局顺序一致性,所有线程看到相同的操作顺序    最低    需要严格顺序保

关键区别总结
同步保证:
relaxed:无同步
acquire-release:配对线程间同步
seq_cst:全局同步
典型模式:
生产者-消费者:生产者用release,消费者用acquire
互斥锁:加锁相当于acquire,解锁相当于release。
acquire:加载操作(读)load,release:存储操作(写)
计数器:可用relaxed
需要严格顺序时用seq_cst

使用 memory_order_acquire 确保状态读取时能看到最新值

使用 memory_order_release 确保状态更新能被其他线程看到   

三、多线程之间如何通信的,进程之间如何通信的
多线程通信:线程之间可以通过共享内存、互斥锁、条件变量、信号量和消息队列等方式进行通信。
进程通信(IPC):进程之间由于各自独立的内存空间,通常通过管道、共享内存、消息队列、套接字、信号等方式进行通信。

四、进程切换和线程切换的区别

进程切换和线程切换是操作系统中两种不同的上下文切换机制,它们的区别主要体现在资源占用、切换开销、通信方式应用场景上。以下是详细对比:


1. 基本概念

(1) 进程(Process)

  • 定义:进程是资源分配的基本单位,拥有独立的地址空间、全局变量、文件描述符、子进程等资源。

  • 特点

    • 进程间相互隔离,一个进程崩溃不会直接影响其他进程。
    • 进程间通信(IPC)需要通过显式机制(如管道、消息队列、共享内存等)。

(2) 线程(Thread)

  • 定义:线程是CPU调度的基本单位,共享进程的资源(如地址空间、全局变量、文件描述符等),但拥有独立的栈、程序计数器、寄存器等上下文。

  • 特点

    • 线程间通信更高效(直接共享内存)。
    • 一个线程崩溃可能导致整个进程崩溃(除非有隔离机制)。

2. 切换开销对比

(1) 进程切换的开销

  • 需要切换的资源

    • 地址空间:切换页表(TLB flush),导致缓存失效。
    • 内核资源:如打开的文件、信号处理表、内存映射等。
    • CPU上下文:寄存器、程序计数器、栈指针等。
  • 开销来源

    • TLB刷新:地址空间切换后,TLB(转换后备缓冲器)中的缓存失效,需重新加载页表。
    • 内核态切换:进程切换通常涉及从用户态到内核态的切换(如通过系统调用或中断)。
    • 缓存失效:CPU缓存(L1/L2/L3)中的数据可能不再有效。
  • 典型时间微秒级到毫秒级(取决于硬件和操作系统优化)。

(2) 线程切换的开销

  • 需要切换的资源

    • CPU上下文:寄存器、程序计数器、栈指针等(与进程切换相同部分)。
    • 线程局部存储(TLS) :部分实现可能需要切换TLS。
  • 无需切换的资源

    • 地址空间:同一进程内的线程共享地址空间,无需切换页表。
    • 内核资源:如文件描述符、全局变量等已共享。
  • 开销来源

    • 寄存器保存/恢复:与进程切换类似,但无TLB刷新和缓存失效。
    • 调度竞争:多线程可能因锁竞争导致额外开销。
  • 典型时间纳秒级到微秒级(比进程切换快10~100倍)。