线程
线程的创建
在 C++ 中,线程的创建核心是通过std::thread
类实现的,其构造函数需要传入一个可调用对象(Callable Object)作为线程入口。可调用对象包括普通函数、lambda 表达式、函数对象(functor)、类的成员函数等。下面详细介绍几种常见的线程创建方式:
一、使用普通函数创建线程
最基础的方式是将普通函数作为线程入口,可同时传递参数给函数。
#include <iostream>
#include <thread>// 普通函数:线程入口
void print_info(int thread_id, const std::string& message) {std::cout << "线程 " << thread_id << ": " << message << std::endl;
}int main() {// 创建线程:传入函数名和参数(参数按顺序传递)std::thread t1(print_info, 1, "Hello from thread 1");std::thread t2(print_info, 2, "Hello from thread 2");// 等待线程完成t1.join();t2.join();return 0;
}
说明:
std::thread
构造时,第一个参数是函数名,后续参数会被传递给该函数。- 若函数需要多个参数,直接在构造函数中按顺序添加即可。
二、使用 lambda 表达式创建线程
lambda 表达式适合编写简短的线程逻辑,尤其当需要捕获外部变量时非常方便。
#include <iostream>
#include <thread>int main() {int base = 100; // 外部变量// 用lambda表达式创建线程(捕获外部变量base)std::thread t([&base](int offset) {// 线程逻辑:使用捕获的base和传入的offsetstd::cout << "线程内计算:" << base + offset << std::endl;}, 50); // 传递给lambda的参数(offset=50)t.join(); // 等待线程完成return 0;
}
说明:
- lambda 的捕获列表(
[&base]
)用于访问外部变量,&
表示按引用捕获(可修改外部变量),=
表示按值捕获(只读)。 - lambda 后的参数(如
50
)会作为 lambda 的输入参数。
三、使用函数对象(Functor)创建线程
函数对象是重载了operator()
的类 / 结构体,适合需要携带状态(成员变量)的线程逻辑。
#include <iostream>
#include <thread>// 函数对象:重载operator()
struct Counter {int count; // 携带的状态// 构造函数初始化状态Counter(int init) : count(init) {}// 线程入口:operator()void operator()(int step) {for (int i = 0; i < 5; ++i) {count += step;std::cout << "当前计数:" << count << std::endl;}}
};int main() {// 创建函数对象(初始状态count=0)Counter counter(0);// 用函数对象创建线程,传递参数step=2std::thread t(std::ref(counter), 2); // 注意用std::ref传递引用t.join();// 线程执行后,counter的状态已被修改std::cout << "最终计数:" << counter.count << std::endl;return 0;
}
说明:
- 函数对象的成员变量(如
count
)可用于存储线程的状态,避免使用全局变量。 - 若需在线程中修改原对象(而非副本),需用
std::ref
传递引用(否则std::thread
会复制对象)。
四、使用类的成员函数创建线程
当线程逻辑需要访问类的成员变量时,可将类的成员函数作为线程入口,需同时指定对象指针。
#include <iostream>
#include <thread>
#include <string>class Worker {
private:std::string name; // 成员变量public:Worker(const std::string& n) : name(n) {}// 成员函数:线程入口void work(int task_id) {std::cout << "工人 " << name << " 正在执行任务 " << task_id << std::endl;}
};int main() {Worker worker("Alice"); // 创建对象// 用成员函数创建线程:参数为(对象指针,成员函数地址,函数参数)std::thread t(&Worker::work, &worker, 1001); // &worker是对象指针t.join();return 0;
}
说明:
std::thread
构造时,第一个参数是成员函数地址(&Worker::work
),第二个参数是对象指针(&worker
),后续参数是成员函数的参数。- 若对象是动态分配的(
new Worker(...)
),则传递堆对象的指针即可。
关键注意事项
线程必须被 join 或 detach:
std::thread
对象销毁前,必须调用join()
(等待线程结束)或detach()
(分离线程,使其独立运行),否则会触发std::terminate()
终止程序。参数传递的拷贝问题:
线程构造时传递的参数会被拷贝到线程内部,若需传递引用,需用std::ref
或std::cref
(常量引用),但需确保引用的对象生命周期长于线程。线程入口的生命周期:
若线程入口是临时对象(如 lambda 或函数对象),需确保其生命周期覆盖线程执行期,避免悬空引用。
总结
C++ 线程创建的核心是通过std::thread
绑定可调用对象,不同方式的适用场景:
- 普通函数:适合简单、无状态的线程逻辑。
- lambda 表达式:适合简短逻辑或需要捕获外部变量的场景。
- 函数对象:适合需要携带状态的复杂逻辑。
- 成员函数:适合面向对象编程中,线程逻辑需访问类成员的场景。
线程的销毁
我们使用std::thread创建的线程对象是进程中的子线程,一般进程中还有主线程,在程序中就是main线程,那么当我们创建线程后至少是有两个线程的,那么两个线程谁先执行完毕谁后执行完毕,这是随机的,但是当进程执行结束之后,主线程与子线程都会执行完毕,进程会回收线程拥有的资源。并且,主线程main执行完毕,其实整个进程也就执行完毕了。一般我们有两种方式让子线程结束,一种是主线程等待子线程执行完毕,我们使用join函数,让主线程回收子线程的资源;另外一种是子线程与主线程分离,我们使用detach函数,此时子线程驻留在后台运行,这个子线程就相当于被C++运行时库接管,子线程执行完毕后,由运行时库负责清理该线程相关的资源。使用detach之后,表明就失去了对子线程的控制。
void func()
{cout << "void func()" << endl;cout << "I'm child thread" << endl;
}void test()
{cout << "I'm main thread" << endl;thread th1(func);th1.join();//主线程等待子线程
}
线程的状态
线程类中有一成员函数joinable,可以用来检查线程的状态。如果该函数为true,表示可以使用join()或者detach()函数来管理线程生命周期。
void test()
{thread t([]{cout << "Hello, world!" << endl;});if (t.joinable()) {t.detach();}
}void test2()
{thread th1([]{cout << "Hello, world!" << endl;});if (t.joinable()) {t.join();}
}
线程id
为了唯一标识每个线程,可以给每个线程一个id,类型为std::thread::id,可以使用成员函数get_id()进行获取。
void test()
{thread th1([](){cout << "子线程ID:" << std::this_thread::get_id() << endl;});th1.join();
}
互斥锁mutex
互斥锁是一种同步原语,用于协调多个线程对共享资源的访问。互斥锁的作用是保证同一时刻只有一个线程可以访问共享资源,其他线程需要等待互斥锁释放后才能访问。在多线程编程中,多个线程可能同时访问同一个共享资源,如果没有互斥锁的保护,就可能出现数据竞争等问题。
然而,互斥锁的概念并不陌生,在Linux下,POSIX标准中也有互斥锁的概念,这里我们说的互斥锁是C++11语法层面提出来的概念,是C++语言自身的互斥锁std::mutex,互斥锁只有两种状态:上锁与解锁。
2、头文件
#include <mutex>
class mutex;
3、常用函数接口
3.1、构造函数
constexpr mutex() noexcept;
mutex( const mutex& ) = delete;
3.2、上锁
void lock();
3.3、尝试上锁
bool try_lock();
3.4、解锁
void unlock();
3.5、使用示例
int gCount = 0;
mutex mtx;//初始化互斥锁
void threadFunc()
{for(int idx = 0; idx < 1000000; ++idx){mtx.lock();//上锁++gCount;mtx.unlock();//解锁}
}
int main(int argc, char *argv[])
{thread th1(threadFunc);thread th2(threadFunc);
th1.join();th2.join();cout << "gCount = " << gCount << endl;return 0;
}
三、lock_guard与unique_lock
在 C++ 多线程编程中,std::lock_guard
和 std::unique_lock
都是用于管理互斥锁(std::mutex
)的RAII 风格工具类,核心作用是自动加锁和解锁,避免手动操作锁导致的死锁(如忘记解锁、异常时未释放锁等问题)。但它们的灵活性和适用场景有显著区别。
一、核心共同点
- 都遵循RAII 原则:构造时获取锁,析构时自动释放锁(无论正常退出还是异常退出)。
- 都用于保护临界区,防止多线程并发访问共享资源导致的数据竞争。
二、关键区别与适用场景
特性 | std::lock_guard | std::unique_lock |
---|---|---|
灵活性 | 简单,功能有限 | 灵活,支持更多操作 |
手动解锁 | 不支持(只能通过析构函数自动解锁) | 支持(通过 unlock() 手动解锁) |
延迟锁定 | 不支持(构造时必须锁定) | 支持(通过 std::defer_lock 延迟锁定) |
尝试锁定 | 不支持 | 支持(通过 std::try_to_lock 尝试锁定) |
所有权转移 | 不支持(不可复制、不可移动) | 支持(可移动,不可复制) |
性能开销 | 更低(轻量级) | 略高(因灵活性带来的额外状态管理) |
适用场景 | 简单临界区(全程需要锁定) | 复杂场景(如条件变量、中途解锁、延迟锁定等) |
三、详细说明与示例
1. std::lock_guard
:简单场景的首选
lock_guard
是轻量级锁管理工具,设计用于最简单的场景:进入临界区时加锁,离开时解锁,全程不需要手动干预。
特点:
- 构造函数必须锁定互斥量(要么直接锁定,要么接受一个已锁定的互斥量,通过
std::adopt_lock
标记)。 - 没有
unlock()
方法,只能在析构时自动解锁(通常是离开作用域时)。 - 不可复制、不可移动,所有权无法转移。
示例:
#include <mutex>
#include <iostream>std::mutex mtx;
int shared_data = 0;void increment() {// 构造时自动锁定mtx,离开作用域(函数结束)时析构,自动解锁std::lock_guard<std::mutex> lock(mtx);// 临界区:安全访问共享资源shared_data++;std::cout << "当前值: " << shared_data << std::endl;// 无需手动解锁,lock析构时自动处理
}
适用场景:
- 临界区逻辑简单,从进入到退出全程需要锁定。
- 不需要中途解锁、延迟锁定等复杂操作。
- 追求最小性能开销。
2. std::unique_lock
:复杂场景的灵活选择
unique_lock
是功能更全面的锁管理工具,支持手动解锁、延迟锁定、尝试锁定等操作,适合需要灵活控制锁状态的场景。
特点:
- 支持延迟锁定:通过
std::defer_lock
标记,构造时不锁定互斥量,后续可通过lock()
手动锁定。 - 支持手动解锁:通过
unlock()
方法中途释放锁,之后可再次通过lock()
重新锁定。 - 支持尝试锁定:通过
std::try_to_lock
标记,尝试锁定互斥量(成功返回 true,失败不阻塞)。 - 支持所有权转移:可通过移动语义(
std::move
)转移锁的所有权(不可复制)。 - 是条件变量(
std::condition_variable
)的必需参数:条件变量的wait()
方法需要unique_lock
作为参数,因为wait()
会在等待时释放锁,被唤醒时重新获取锁(这要求锁可以手动解锁和锁定)。
示例 1:延迟锁定与手动解锁
#include <mutex>
#include <iostream>std::mutex mtx;void complex_operation() {// 延迟锁定:构造时不锁定,仅关联互斥量std::unique_lock<std::mutex> lock(mtx, std::defer_lock);// 做一些不需要锁定的操作std::cout << "准备锁定..." << std::endl;// 手动锁定lock.lock();std::cout << "已锁定,执行临界区操作..." << std::endl;// 中途手动解锁(释放锁,允许其他线程访问)lock.unlock();std::cout << "临时解锁,执行其他操作..." << std::endl;// 再次锁定lock.lock();std::cout << "再次锁定,完成剩余操作..." << std::endl;// 析构时自动解锁(若当前处于锁定状态)
}
示例 2:与条件变量配合
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;void consumer() {std::unique_lock<std::mutex> lock(mtx);// 等待条件满足:会释放锁并阻塞,被唤醒时重新获取锁cv.wait(lock, []{ return data_ready; });// 条件满足,执行消费操作std::cout << "数据已准备好,开始处理..." << std::endl;
}void producer() {{std::lock_guard<std::mutex> lock(mtx);data_ready = true; // 生产数据} // 离开作用域,自动解锁cv.notify_one(); // 通知消费者
}int main() {std::thread t1(consumer);std::thread t2(producer);t1.join();t2.join();return 0;
}
适用场景:
- 需要中途解锁(如临界区中间有耗时操作但无需锁定)。
- 需要延迟锁定(如先做准备工作,再根据条件决定是否锁定)。
- 需要与条件变量配合(
wait()
必须使用unique_lock
)。 - 需要转移锁的所有权(如将锁传递给其他函数)。
四、总结
- 优先使用
std::lock_guard
:当场景简单,临界区全程需要锁定时,它更轻量、更高效。 - 使用
std::unique_lock
:当需要灵活性(手动解锁、延迟锁定、配合条件变量等)时,牺牲少量性能换取功能。
两者的核心目标都是安全管理锁的生命周期,避免手动操作锁导致的错误,选择时主要依据场景的复杂度和灵活性需求。
条件变量condition_variable
在 C++ 多线程编程中,std::condition_variable
(条件变量)是用于线程间同步的核心机制,它允许线程在满足特定条件前阻塞等待,当条件满足时被其他线程唤醒,从而实现高效的协作(避免无效轮询)。
一、核心作用
条件变量解决的核心问题:让线程在 “条件不满足” 时进入休眠状态,在 “条件满足” 时被唤醒继续执行,避免线程通过 “轮询”(反复检查条件)浪费 CPU 资源。
例如:
- 消费者线程等待生产者线程生成数据(“数据就绪” 是条件)。
- 主线程等待子线程完成初始化(“初始化完成” 是条件)。
二、核心 API 与工作机制
std::condition_variable
定义在 <condition_variable>
头文件中,核心方法如下:
方法 | 作用 |
---|---|
wait(lock, pred) | 阻塞当前线程,释放锁并等待被唤醒;被唤醒后重新获取锁,检查pred 是否为true ,若为true 则继续执行,否则重新阻塞。 |
notify_one() | 唤醒一个正在等待该条件变量的线程(若有)。 |
notify_all() | 唤醒所有正在等待该条件变量的线程。 |
关键细节:
必须配合互斥锁:条件变量的操作必须与互斥锁(
std::mutex
)结合,且必须使用std::unique_lock
(而非std::lock_guard
),因为wait()
过程需要先释放锁、被唤醒后重新获取锁(unique_lock
支持手动解锁 / 加锁,lock_guard
不支持)。处理 “虚假唤醒”:操作系统可能在无明确通知时唤醒线程(虚假唤醒),因此
wait()
必须配合条件谓词(pred) 使用,确保只有当条件真正满足时才继续执行。
三、工作流程(以生产者 - 消费者为例)
消费者线程:
- 锁定互斥锁,检查条件(如 “数据是否就绪”)。
- 若条件不满足,调用
wait()
:释放锁并阻塞等待。 - 被唤醒后,重新获取锁,再次检查条件(避免虚假唤醒)。
- 条件满足时,执行操作(如消费数据)。
生产者线程:
- 锁定互斥锁,修改共享资源(如生成数据)。
- 调用
notify_one()
或notify_all()
唤醒等待的消费者。 - 释放锁(由
unique_lock
或lock_guard
自动完成)。
四、完整示例:生产者 - 消费者模型
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>// 共享队列(缓冲区)
std::queue<int> buffer;
const int MAX_SIZE = 5; // 缓冲区最大容量// 同步工具
std::mutex mtx;
std::condition_variable cv_producer; // 生产者等待的条件变量(缓冲区不满)
std::condition_variable cv_consumer; // 消费者等待的条件变量(缓冲区非空)// 生产者:向缓冲区添加数据
void producer(int id) {for (int i = 0; i < 10; ++i) {std::unique_lock<std::mutex> lock(mtx);// 等待缓冲区不满(若满则阻塞)cv_producer.wait(lock, []{ return buffer.size() < MAX_SIZE; });// 生产数据int data = id * 100 + i;buffer.push(data);std::cout << "生产者 " << id << " 生产: " << data << ",缓冲区大小: " << buffer.size() << std::endl;// 通知消费者:缓冲区非空cv_consumer.notify_one();// 模拟生产耗时std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}// 消费者:从缓冲区取出数据
void consumer(int id) {for (int i = 0; i < 10; ++i) {std::unique_lock<std::mutex> lock(mtx);// 等待缓冲区非空(若空则阻塞)cv_consumer.wait(lock, []{ return !buffer.empty(); });// 消费数据int data = buffer.front();buffer.pop();std::cout << "消费者 " << id << " 消费: " << data << ",缓冲区大小: " << buffer.size() << std::endl;// 通知生产者:缓冲区不满cv_producer.notify_one();// 模拟消费耗时std::this_thread::sleep_for(std::chrono::milliseconds(150));}
}int main() {// 创建2个生产者和2个消费者std::thread p1(producer, 1);std::thread p2(producer, 2);std::thread c1(consumer, 1);std::thread c2(consumer, 2);// 等待所有线程完成p1.join();p2.join();c1.join();c2.join();return 0;
}
五、关键注意事项
必须使用
unique_lock
:wait()
方法的第一个参数必须是std::unique_lock<std::mutex>
,因为wait()
内部会执行 “解锁→阻塞→被唤醒后重新加锁” 的操作,unique_lock
支持这种灵活的锁状态管理(lock_guard
不支持手动解锁,无法配合wait()
)。条件谓词不可省略:即使你认为 “不会有虚假唤醒”,也必须在
wait()
中传入条件谓词(第二个参数)。例如:// 错误:未处理虚假唤醒 cv.wait(lock); // 正确:确保条件满足才继续 cv.wait(lock, []{ return condition; });
notify_one()
与notify_all()
的选择:notify_one()
:唤醒一个等待线程,适用于 “只有一个线程能处理” 的场景(如缓冲区只有一个数据)。notify_all()
:唤醒所有等待线程,适用于 “多个线程都能处理” 的场景(如广播一个全局事件)。过度使用notify_all()
可能导致线程唤醒后竞争锁,浪费资源。
避免持有锁时长时间操作:唤醒线程后,应尽快释放锁(完成临界区操作),避免其他线程被唤醒后因无法获取锁而阻塞。
生命周期管理:确保条件变量在所有等待线程退出前保持有效,避免访问已销毁的条件变量。
六、总结
std::condition_variable
是多线程协作的高效工具,通过 “等待 - 通知” 机制替代轮询,减少 CPU 浪费。其核心是:线程在条件不满足时阻塞,条件满足时被唤醒,配合互斥锁和条件谓词确保同步安全。典型应用包括生产者 - 消费者模型、线程池任务调度、事件驱动同步等。