C++ 多线程笔记
C++ 多线程笔记
多线程的执行过程是怎样的呢?
1. 多线程执行过程
- 在多核场景下,线程可以被调度到不同核心上真正并行执行,此时没有切换开销。
- 当线程数量超过 CPU 的逻辑核心数时,系统通过时间片和抢占式调度来实现并发。
- 线程切换的核心是上下文切换:内核会保存当前线程的硬件上下文(寄存器、栈、程序计数器等)到其线程控制块(TCB)中,然后将下一个待执行线程的上下文恢复到 CPU,从而完成切换。
- 触发切换的常见情形:
- 时间片耗尽
- 线程因等待锁、I/O 操作、sleep 等而进入阻塞状态
- 高优先级线程的抢占
频繁的上下文切换会带来显著开销,这是实际开发中需要控制线程数量、尽量减少切换次数的重要原因,也是线程池能够提升效率的核心原因之一。
2. 线程分离(detach)
- 若使用
detach方式创建线程,请避免向线程入口函数传递引用或指针类型的参数。 - 使用临时对象作为实参,可以确保线程入口函数所需参数在主函数
main退出前就已构造完成,从而安全使用。
3. 线程标识
- 获取当前线程ID:
std::this_thread::get_id()
4. 互斥量(mutex)
4.1 lock 与 unlock
基本使用规则是成对调用:每调用一次 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 可用于直接替代手动调用 lock 和 unlock。使用了 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_lock或lock_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;
}
执行逻辑:
- 消费者线程调用
wait,若条件(队列非空)满足则继续执行;否则释放锁并阻塞。 - 生产者线程获得锁,放入数据后调用
notify_one()唤醒一个等待的消费者。 - 被唤醒的消费者重新获得锁,检查条件,若满足则继续执行消费操作。
5.2 notify_all
通知所有正在等待的线程。将上述示例中的 my_cond.notify_one() 替换为 my_cond.notify_all() 即可。注意,即使通知了所有线程,同一时刻也只有一个线程能获得锁并执行。
6. 异步任务与返回值
6.1 std::async 与 std::future
std::thread 创建的线程没有直接的返回值获取机制,而 std::async 和 std::future 可以解决此问题。
std::async是一个函数模板,用于启动一个异步任务(可能在新线程中执行)。- 它返回一个
std::future对象,该对象持有异步任务的结果。 - 通过
future的get()或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::future的wait/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()方法可以判断future的get方法是否已被调用过。
6.6 std::shared_future
std::future 的 get 是移动语义,只能调用一次。std::shared_future 的 get 是浅复制语义,允许多次调用,多个线程可以共享同一个异步任务的结果。它采用引用计数管理生命周期。
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)可能就不是原子的。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)