C++ 多线程笔记

多线程的执行过程是怎样的呢?

1. 多线程执行过程

  • 在多核场景下,线程可以被调度到不同核心上真正并行执行,此时没有切换开销。
  • 当线程数量超过 CPU 的逻辑核心数时,系统通过时间片和抢占式调度来实现并发。
  • 线程切换的核心是上下文切换:内核会保存当前线程的硬件上下文(寄存器、栈、程序计数器等)到其线程控制块(TCB)中,然后将下一个待执行线程的上下文恢复到 CPU,从而完成切换。
  • 触发切换的常见情形:
    • 时间片耗尽
    • 线程因等待锁、I/O 操作、sleep 等而进入阻塞状态
    • 高优先级线程的抢占

频繁的上下文切换会带来显著开销,这是实际开发中需要控制线程数量、尽量减少切换次数的重要原因,也是线程池能够提升效率的核心原因之一。

2. 线程分离(detach)

  • 若使用 detach 方式创建线程,请避免向线程入口函数传递引用或指针类型的参数。
  • 使用临时对象作为实参,可以确保线程入口函数所需参数在主函数 main 退出前就已构造完成,从而安全使用。

3. 线程标识

  • 获取当前线程ID:std::this_thread::get_id()

4. 互斥量(mutex)

4.1 lockunlock

基本使用规则是成对调用:每调用一次 lock,必须对应调用一次 unlock。调用次数不匹配(例如调用一次 lock 却调用两次 unlock,或反之)将导致程序行为不稳定甚至崩溃。

void inMsgRecvQueue() {
    for (int i = 0; i < 100000; i++) {
        std::cout << "inMsgRecvQueue() 执行,插入一个元素" << i << std::endl;
        my_mutex.lock();
        msgRecvQueue.push_back(i);
        my_mutex.unlock();
    }
}

4.2 std::lock_guard 类模板

std::lock_guard 可用于直接替代手动调用 lockunlock。使用了 lock_guard 后,便不应再手动调用这两个函数。

工作原理:在其构造函数中调用互斥量的 lock 成员函数,在析构函数中调用互斥量的 unlock 成员函数。

bool outMsgLULProc(int& command) {
    std::lock_guard<std::mutex> sbguard(my_mutex);
    if (!msgRecvQueue.empty()) {
        command = msgRecvQueue.front();
        msgRecvQueue.pop_front();
        return true;
    }
    return false;
}

4.3 死锁

死锁通常发生在涉及至少两个互斥量的场景中。例如:

  • 线程 A 先锁住互斥量 m1,然后尝试锁 m2。
  • 在线程 A 锁住 m2 之前,发生上下文切换,线程 B 开始执行。
  • 线程 B 先锁住互斥量 m2,然后尝试锁 m1。
  • 此时线程 A 等待线程 B 释放 m2,线程 B 等待线程 A 释放 m1,形成死锁。

解决方法:从程序设计逻辑入手,避免出现循环等待条件。

4.4 std::lock 函数模板

std::lock 可以一次性锁定两个或更多互斥量(数量不能为1),它通过内部算法避免了因加锁顺序不同而可能引发的死锁风险。

工作方式:它会尝试锁定所有指定的互斥量。如果其中任何一个未能锁定,它会释放所有已锁定的互斥量并等待,直到所有互斥量都能被锁定,然后才返回。

void inMsgRecvQueue() {
    for (int i = 0; i < 100000; i++) {
        std::cout << "inMsgRecvQueue() 执行,插入一个元素 " << i << std::endl;
        std::lock(my_mutex, my_mutex2);
        msgRecvQueue.push_back(i);
        my_mutex.unlock(); // 此时谁先解锁均无关系
        my_mutex2.unlock();
    }
}

bool outMsgLULProc(int& command) {
    std::lock(my_mutex, my_mutex2);
    if (!msgRecvQueue.empty()) {
        command = msgRecvQueue.front();
        msgRecvQueue.pop_front();
        my_mutex.unlock(); // 此时谁先解锁均无关系
        my_mutex2.unlock();
        return true;
    }
    my_mutex.unlock(); // 此时谁先解锁均无关系
    my_mutex2.unlock();
    return false;
}

4.5 unique_lock 详解

std::unique_lock 是一个比 std::lock_guard 更灵活的类模板,两者都用于管理互斥量的加锁与解锁。lock_guard 在构造时加锁,析构时解锁,灵活性较低。unique_lock 提供了更多控制选项,但性能开销和内存占用也略高。

  • 可完全替代unique_lock 可以完全取代 lock_guard
  • 第二参数 std::adopt_lock:这是一个标记,表明调用者线程已经拥有该互斥量的所有权(即已手动调用 lock),unique_locklock_guard 在构造时将不再尝试加锁。
std::unique_lock<std::mutex> sbguard1(my_mutex, std::adopt_lock);
  • 第二参数 std::try_to_lock:指示 unique_lock 尝试加锁,若失败则立即返回而不阻塞。前提是程序员不能事先手动锁定该互斥量。
void inMsgRecvQueue() {
    for (int i = 0; i < 100000; i++) {
        std::cout << "inMsgRecvQueue() 执行,插入一个元素 " << i << std::endl;
        std::unique_lock<std::mutex> sbguard(my_mutex, std::try_to_lock);
        if (sbguard.owns_lock()) {
            msgRecvQueue.push_back(i);
        } else {
            // 处理未获得锁的情况
        }
    }
}

5. 信号量(std::condition_variable

std::condition_variable 用于线程间同步,类似于“闹钟+休息室”机制。它可避免线程空转浪费 CPU 资源,实现精准的等待/唤醒,常用于生产者-消费者模型:

  • 生产者线程向队列放入数据
  • 消费者线程从队列取出数据
  • 当队列无数据时,消费者应休眠等待通知,而非忙等待
  • 当生产者放入数据后,应通知唤醒消费者

5.1 notify_one

通知一个正在等待的线程。

#include <mutex>
#include <vector>
#include <iostream>
#include <thread>

std::mutex m_mutex;
std::condition_variable my_cond;
std::vector<int> m_msgRecvQueue;

void outMsgRecvQueue() {
    int command = 0;
    while (true) {
        std::unique_lock<std::mutex> lock(m_mutex);
        my_cond.wait(lock, [&]() {
            if (!m_msgRecvQueue.empty()) // 解决虚假唤醒问题
                return true;
            return false;
        });
        command = m_msgRecvQueue.back();
        m_msgRecvQueue.pop_back();
        lock.unlock();
        std::cout << "outMsgRecvQueue()执行,取出一个元素 " << command << "=====" << std::endl;
    }
}

void inMsgRecvQueue() {
    for (int i = 0; i < 10; i++) {
        std::cout << "inMsgRecvQueue()执行,插入一个元素 " << i << "=====" << std::endl;
        std::unique_lock<std::mutex> lock(m_mutex);
        m_msgRecvQueue.emplace_back(i);
        my_cond.notify_one();
    }
}

int main() {
    std::thread t1(outMsgRecvQueue);
    std::thread t2(inMsgRecvQueue);
    std::thread t3(outMsgRecvQueue);
    std::thread t4(inMsgRecvQueue);
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    return 0;
}

执行逻辑

  1. 消费者线程调用 wait,若条件(队列非空)满足则继续执行;否则释放锁并阻塞。
  2. 生产者线程获得锁,放入数据后调用 notify_one() 唤醒一个等待的消费者。
  3. 被唤醒的消费者重新获得锁,检查条件,若满足则继续执行消费操作。

5.2 notify_all

通知所有正在等待的线程。将上述示例中的 my_cond.notify_one() 替换为 my_cond.notify_all() 即可。注意,即使通知了所有线程,同一时刻也只有一个线程能获得锁并执行。

6. 异步任务与返回值

6.1 std::asyncstd::future

std::thread 创建的线程没有直接的返回值获取机制,而 std::asyncstd::future 可以解决此问题。

  • std::async 是一个函数模板,用于启动一个异步任务(可能在新线程中执行)。
  • 它返回一个 std::future 对象,该对象持有异步任务的结果。
  • 通过 futureget()wait() 成员函数可获取结果或等待完成。
  • 注意get() 只能调用一次,多次调用会抛出异常。get() 会阻塞等待结果,而 wait() 仅阻塞等待完成。
#include <future>
#include <iostream>
#include <thread>
#include <chrono>

class A {
public:
    int myThread() {
        std::cout << "myThread() start " << "threadid = " << std::this_thread::get_id() << std::endl;
        std::chrono::milliseconds dura(2000);
        std::this_thread::sleep_for(dura);
        std::cout << "myThread() end " << "threadid = " << std::this_thread::get_id() << std::endl;
        return 888;
    }
};

int main() {
    A a;
    std::future<int> result = std::async(&A::myThread, &a);
    std::cout << "执行结果为: " << result.get() << std::endl;
    return 0;
}

6.2 std::async 额外参数

可以给 std::async 提供一个额外的参数,类型为 std::launch(枚举类型),来控制其行为。

  • std::launch::deferred:表示线程入口函数的执行被延迟到 std::futurewait/get 函数调用时。如果 wait/get 没有被调用,则这个线程就不执行了。
  • std::launch::async:表示在调用 async 函数时就强制创建新线程并开始执行。
  • std::launch::deferred | std::launch::async:表示由系统决定是创建新线程立即执行,还是延迟到调用 get/wait 时再(在主线程中)同步执行。
  • 不使用额外参数:相当于使用了 std::launch::deferred | std::launch::async

6.3 std::packaged_task

std::packaged_task 用于把函数或可调用对象打包成任务,并自动关联一个 std::future 来获取任务的结果。

#include <future>
#include <iostream>
#include <thread>

int mythread(int i) { 
    // ... 做一些计算
    return i * 2;
}

int main() {
    // 将函数打包成任务
    std::packaged_task<int(int)> mypt(mythread);
    // 在新线程中执行任务
    std::thread t1(std::ref(mypt), 1);
    t1.join();
    // 获取关联的future并取得结果
    std::future<int> result = mypt.get_future();
    std::cout << result.get() << std::endl;
    return 0;
}

它也支持包装 lambda 表达式和存储在容器中。

6.4 std::promise

std::promise 是一个类模板,用于在线程间传递值。一个线程可以通过它设置值(set_value),而另一个线程可以通过与之关联的 std::future 来获取这个值(get)。其 get 方法会阻塞等待,属于内核级等待,不浪费 CPU 资源。

#include <future>
#include <iostream>
#include <thread>

void mythread(std::promise<int>& tmpp, int calc) {
    calc++;
    int result = calc;
    tmpp.set_value(result); // 在线程中设置值
}

int main() {
    std::promise<int> myprom;
    std::thread t1(mythread, std::ref(myprom), 180);

    t1.join(); // join 必须调用,位置在 get 前后均可

    std::future<int> ful = myprom.get_future();
    auto result = ful.get(); // 在主线程中获取值
    std::cout << "result = " << result << std::endl;
    return 0;
}

注意get() 方法只能调用一次。

6.5 std::future 的其他方法

  • 等待与超时:使用 wait_for 可以设置超时等待,返回 std::future_status 状态。
    • std::future_status::timeout:超时,线程未执行完。
    • std::future_status::ready:线程已成功返回。
    • std::future_status::deferred:线程被延迟执行(对应 std::launch::deferred)。
  • 有效性检查valid() 方法可以判断 futureget 方法是否已被调用过。

6.6 std::shared_future

std::futureget 是移动语义,只能调用一次。std::shared_futureget 是浅复制语义,允许多次调用,多个线程可以共享同一个异步任务的结果。它采用引用计数管理生命周期。

std::packaged_task<int(int)> mypt(mythread);
std::thread t1(std::ref(mypt), 1);
t1.join();
std::future<int> result = mypt.get_future();

// 将 future 转换为 shared_future
std::shared_future<int> result_s(std::move(result)); // 或 result_s = result.share()

auto res1 = result_s.get(); // 可以多次调用 get
auto res2 = result_s.get(); // 不会报错

7. 原子操作 std::atomic

std::atomic 是一个类模板,用于封装一个类型的值,并提供不可分割的(原子的)读写操作。

std::atomic<int> g_mycount = 0;

注意:对于 ++--+=-=&=|=^= 等简单运算符的运算是原子的,但更复杂的表达式运算(如 g_mycount = g_mycount + 1)可能就不是原子的。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐