c++多线程详解
文章目录
- c++多线程详解
- 一. thread
- thread
- 创建
- join
- detach
- this_thread
- get_id
- yieid
- sleep_for
- sleep_until
- 二. mutex
- mutex
- lock_guard
- unique_lock
- 三. atomic
- atomic
- atomic_flag
- 四. condition_variable / condition_variable_any
- wait
- wait_for
- wait_until
- notify_one
- notify_all
- 五. future
- future
- shared_future
- promise
- packaged_task
一. thread
头文件
#include <thread>
thread
创建
线程的创建很简单,只需要将对应的函数添加到线程当中即可
常见以下几种创建方式:
void thread_func()
{
std::cout << "t1" << std::endl;
}
std::thread th1(thread_func); //没有参数
void thread_func(int x)
{
std::cout << "t2:" << x << std::endl;
}
std::thread th2(thread_func,100);//带参数
void thread_func(double x)
{
while (1)
{
std::cout << "t3:" << x << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
std::thread (thread_func, 100).detach();//没有线程名字
demo:
#include <iostream>
#include <thread>
#include <chrono>
void test_t1()
{
std::cout << "t1" << std::endl;
}
void test_t2(int x)
{
std::cout << "t2:" << x << std::endl;
}
void test_t3(double x)
{
while (1)
{
std::cout << "t3:" << x << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main() {
std::thread th1(test_t1);
std::thread th2(test_t2,100);
std::thread (test_t3, 100).detach();
th1.join();
th2.join();
std::cout << "end" << std::endl;
}
join
当线程启动后,在该线程销毁前,确定以join的方式等待线程执行结束。
join方式:即等待模式,等待该线程结束,才会继续往下执行。
通常搭配joinable()判断该线程是否处于可等待的状态(如果线程已经结束或者线程被detach了则无法join)。
demo:
#include <iostream>
#include <thread>
#include <chrono>
void test_t1()
{
std::cout << "线程开始" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5)); //等待5秒
std::cout << "5秒之后" << std::endl;
}
int main() {
std::thread th1(test_t1);
if(th1.joinable())
{
th1.join();
}
std::cout << "end" << std::endl;
}
detach
detach方式:即分离模式,该线程自主在后台运行,当前的代码继续往下执行,不等待该线程结束。
#include <iostream>
#include <thread>
#include <chrono>
void test_t1()
{
std::cout << "线程开始" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5)); //等待5秒
std::cout << "5秒之后" << std::endl;
}
int main() {
std::thread th1(test_t1);
th1.detach();
std::cout << "end" << std::endl; //这里不会等待线程直接运行结束
}
this_thread
get_id
获取线程id
std::this_thread::get_id() 获取线程id
#include <iostream>
#include <thread>
#include <chrono>
void test_t1()
{
std::cout << "thread_id:" << std::this_thread::get_id() << std::endl;
std::cout << "线程开始" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5)); //等待5秒
std::cout << "5秒之后" << std::endl;
}
int main() {
std::thread th1(test_t1);
th1.join();
std::cout << "end" << std::endl;
}
yieid
让当前线程主动放弃 CPU 的执行权
注意:
- 这里并不是终结该线程的意思。
- 当调用 std::this_thread::yield() 时,当前线程会向调度器请求放弃其当前的时间片。调度器可以选择将 CPU 分配给其他可运行的线程。
- 这在某些情况下是有用的,尤其是在竞争条件下,可以减少当前线程的 CPU 占用,允许其他线程执行。
- 在需要频繁检查某个条件的循环中,如果当前线程没有条件满足时,调用 yield() 可以使其他线程有机会运行。这有助于提高程序的响应性,尤其是在多线程环境下。
- 在一些实时系统中,使用 yield() 可以帮助避免优先级反转问题,因为它允许较低优先级的线程让出执行权,从而使较高优先级的线程能尽快运行。
- std::this_thread::yield() 的行为依赖于底层操作系统的线程调度策略,不同操作系统可能有不同的实现。
- 过度使用 yield() 可能导致性能下降,因为频繁让出 CPU 时间可能导致线程切换的开销大于潜在的收益。
#include <iostream>
#include <thread>
#include <chrono>
void test_t1()
{
std::cout << "线程t1开始" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5)); //等待5秒
std::cout << "t1线程5秒之后" << std::endl;
std::this_thread::yield();//提示操作系统可以调度其他线程,如果没有其他可运行的线程或当前线程仍然被调度器选中,它将继续执行。
std::this_thread::sleep_for(std::chrono::seconds(5)); //等待5秒
std::cout << "t1线程10秒之后" << std::endl;
}
void test_t2()
{
std::cout << "线程t2开始" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5)); //等待5秒
std::cout << "t2线程5秒之后" << std::endl;
}
int main() {
std::thread th1(test_t1);
std::thread th2(test_t2);
th1.join();
th2.join();
std::cout << "end" << std::endl;
}
sleep_for
让当前线程休眠指定时间段,可以是 std::chrono::milliseconds、std::chrono::seconds、std::chrono::microseconds 等
std::this_thread::sleep_for(std::chrono::seconds(5)); //等待5秒
std::this_thread::sleep_for(std::chrono::milliseconds(5)); //等待5毫秒
sleep_until
让当前线程暂停执行,直到指定的时间点。
通常使用 std::chrono::steady_clock 或 std::chrono::system_clock 来指定时间点。
示例 1:使用 steady_clock
#include <iostream>
#include <thread>
#include <chrono>
int main() {
auto wake_time = std::chrono::steady_clock::now() + std::chrono::seconds(3); // 设定3秒后的时间点
std::cout << "Sleep!" << std::endl;
std::this_thread::sleep_until(wake_time); // 休眠直到设定的时间点
std::cout << "Awake!" << std::endl;
return 0;
}
示例 2:使用 system_clock
#include <iostream>
#include <thread>
#include <chrono>
int main() {
auto wake_time = std::chrono::system_clock::now() + std::chrono::seconds(3); // 设定3秒后的时间点
std::cout << "Sleep!" << std::endl;
std::this_thread::sleep_until(wake_time); // 休眠直到设定的时间点
std::cout << "Awake!" << std::endl;
return 0;
}
二. mutex
#include <mutex> //头文件
std::mutex 是 C++11 标准库中提供的一个同步原语,用于保护共享数据的互斥访问,防止多个线程同时访问同一资源,从而导致数据竞争和不一致的状态。
mutex
- 基本概念
- 互斥锁:std::mutex 提供了一种互斥机制,使得在任何时刻只有一个线程可以拥有锁,从而安全地访问共享资源。
- 锁的状态:std::mutex
有两种状态:锁定(locked)和未锁定(unlocked)。当一个线程锁定一个互斥量后,其他尝试锁定该互斥量的线程将被阻塞,直到第一个线程释放锁。
- 常用成员函数
- lock():请求锁定互斥量。如果互斥量已经被其他线程锁定,则调用线程会阻塞,直到锁被释放。
- unlock():释放互斥量。如果调用线程没有持有该锁,则会引发未定义行为。
- try_lock():尝试锁定互斥量。如果成功,则返回 true;如果互斥量已被锁定,则返回 false,并且不会阻塞线程。
注意: 在使用 lock() 和 unlock() 时,如果在 lock() 和 unlock() 之间发生异常,可能会导致互斥量永远被锁定。为了解决这个问题,可以使用 std::lock_guard 或 std::unique_lock,它们会在作用域结束时自动释放锁。
示例:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
std::mutex mtx; // 创建一个互斥量
int shared_counter = 0; // 共享资源
void increment_counter(int id) {
for (int i = 0; i < 10; ++i) {
mtx.lock(); // 请求锁定
std::cout << "Thread " << id << " add.\n";
++shared_counter; // 修改共享资源
mtx.unlock(); // 释放锁
}
std::cout << "Thread " << id << " finished incrementing.\n";
}
int main() {
const int num_threads = 10;
std::vector<std::thread> threads;
// 创建多个线程
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(increment_counter, i);
}
// 等待所有线程完成
for (auto& th : threads) {
th.join();
}
std::cout << "Final counter value: " << shared_counter << "\n";
return 0;
}
lock_guard
使用 std::lock_guard 可以确保在作用域结束时自动释放锁,提供更好的异常安全性。
特点:
- RAII:std::lock_guard 使用 RAII 原则,即资源的获取与释放在对象的生命周期内自动管理。创建 std::lock_guard 对象时,它会自动锁定给定的互斥量,而在对象析构时(即作用域结束时),它会自动释放锁。
- 构造与析构:std::lock_guard 的构造函数会调用互斥量的 lock() 方法,而析构函数会调用 unlock() 方法。这确保了即使在发生异常时,也能正确释放锁。
- std::lock_guard 禁用拷贝构造和移动构造,以避免复制对象时误解锁或重复锁定。
- 不能中途解锁,必须等作用域结束才解锁
示例:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
std::mutex mtx; // 创建一个互斥量
int shared_counter = 0; // 共享资源
void increment_counter_safe(int id) {
for (int i = 0; i < 10; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 自动管理锁
std::cout << "Thread " << id << " add.\n";
++shared_counter;
}
std::cout << "Thread " << id << " finished incrementing.\n";
}
int main() {
const int num_threads = 10;
std::vector<std::thread> threads;
// 创建多个线程
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(increment_counter_safe, i);
}
// 等待所有线程完成
for (auto& th : threads) {
th.join();
}
std::cout << "Final counter value: " << shared_counter << "\n";
return 0;
}
unique_lock
std::unique_lock 是 C++11 中引入的一个灵活的互斥量管理工具,属于 RAII(Resource Acquisition Is Initialization)风格的锁管理类。与 std::lock_guard 不同,std::unique_lock 提供了更多的功能和灵活性,适用于需要更复杂锁管理场景的情况。
- 基本概念
- RAII 原则:与 std::lock_guard 一样,std::unique_lock 通过对象的生命周期管理互斥量的锁定和解锁。创建 std::unique_lock 对象时,它会自动锁定给定的互斥量,而在对象析构时会自动释放锁。
- 灵活性:std::unique_lock 提供了可延迟锁定、手动锁定和解锁的能力,还支持锁的转移等高级特性。
- 构造与析构
- 构造函数:可以在创建时锁定互斥量,也可以选择不立即锁定。可以通过 std::defer_lock 参数实现延迟锁定。
- 析构函数:在 std::unique_lock 对象销毁时,自动释放互斥量的锁。
- 常用成员函数
- lock():手动锁定互斥量。
- unlock():手动解锁互斥量。
- try_lock():尝试锁定互斥量,如果成功返回 true,否则返回 false。
- release():释放对互斥量的控制,将锁的所有权转移给调用者。之后该 unique_lock 对象将不再负责解锁。
- swap():交换两个 std::unique_lock 对象的状态。
- 注意事项
- 可延迟锁定:std::unique_lock 可以通过 std::defer_lock 参数构造,在需要时再手动调用 lock(),适合复杂的锁定逻辑。
- 手动解锁:在某些情况下,可能需要在一个锁的作用域内解锁,以便在该作用域中进行其他操作。可以通过调用 unlock() 实现。
- 异常安全性:使用 std::unique_lock 也能提高异常安全性,因为即使在函数中出现异常,互斥量仍能被正确解锁。
- 与 std::lock_guard 的比较
- 灵活性:std::unique_lock 比 std::lock_guard 更灵活,支持手动锁定、解锁和延迟锁定。
- 锁的转移:std::unique_lock 可以通过 release() 转移锁的拥有权,而 std::lock_guard 不支持这一特性。
- 总结
std::unique_lock 是一个功能强大的互斥量管理工具,适用于需要复杂锁管理的场景。通过灵活的锁定和解锁策略,可以在多线程程序中有效地保护共享资源,提高代码的安全性和可维护性。使用 std::unique_lock 能够更好地满足多样化的线程同步需求。
延时锁定的示例:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtx; // 创建一个互斥量
void thread_function() {
// 使用 std::defer_lock 创建一个未锁定的 unique_lock
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// 模拟一些工作
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// 根据条件决定何时锁定
lock.lock(); // 手动锁定
std::cout << "Thread has acquired the lock.\n";
// 模拟一些工作
std::this_thread::sleep_for(std::chrono::seconds(1));
// 自动解锁,当 lock 的作用域结束时
std::cout << "Thread is releasing the lock.\n";
}
int main() {
// 创建多个线程
std::thread t1(thread_function);
std::thread t2(thread_function);
// 等待线程完成
t1.join();
t2.join();
return 0;
}
三. atomic
#include <atomic> //头文件atomic
std::atomic 是 C++11 中引入的一个重要特性,提供了原子操作的支持,使得多线程编程变得更加安全和高效。原子操作是指在多个线程中执行时不会被中断的操作,这意味着在执行时要么完全成功,要么完全失败,不会出现中间状态。
atomic
- 基本概念
原子性:确保操作在多个线程中是不可分割的,其他线程无法在操作执行时观察到中间状态。
内存序:std::atomic 允许你控制内存操作的顺序,提供了多种内存序模型,如顺序一致性、释放-获取等。 - std::atomic 的基本用法
- 2.1. 声明和初始化
你可以使用 std::atomic 声明基本类型的原子变量。例如:
#include <atomic>
std::atomic<int> counter(0);
这里,counter 是一个原子整数,初始值为 0。
- 2.2 原子操作
std::atomic 提供了一系列的原子操作,包括读取、写入和修改。常用的方法有:
加载和存储:
int value = counter.load(); // 原子读取
counter.store(10); // 原子写入
原子增加和减少:
counter.fetch_add(1); // 原子增加 1,返回旧值
counter.fetch_sub(1); // 原子减少 1,返回旧值
比较并交换(CAS):
int expected = 0;
int desired = 1;
if (counter.compare_exchange_strong(expected, desired)) {
// 如果 counter 当前值为 expected,则将其设置为 desired
}
-
原子类型的分类
C++11 提供了多种类型的原子变量,包括:整数类型:std::atomic<int>,std::atomic<unsigned int>,std::atomic<long>,等。 指针类型:std::atomic<T*>,用于指向对象的指针。 布尔类型:std::atomic<bool>,用于表示布尔值。
-
内存序模型
std::atomic 提供了多种内存序选项,允许开发者控制操作的可见性和顺序。常用的内存序包括:顺序一致性 (std::memory_order_seq_cst):默认内存序,提供全局顺序一致性。 释放-获取 (std::memory_order_release 和 std::memory_order_acquire):确保在释放和获取操作之间的依赖关系。 松散顺序 (std::memory_order_relaxed):不提供同步,但保证原子性。
使用示例:
counter.store(10, std::memory_order_relaxed); // 使用松散顺序存储
int value = counter.load(std::memory_order_acquire); // 获取值时使用获取顺序
示例:
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
std::atomic<int> counter(0);
void increment_counter(int n) {
for (int i = 0; i < n; ++i) {
counter.fetch_add(1); // 原子增加
}
}
int main() {
const int num_threads = 10;
const int increments_per_thread = 1000;
std::vector<std::thread> threads;
// 启动多个线程
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(increment_counter, increments_per_thread);
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
std::cout << "Final counter value: " << counter.load() << std::endl; // 输出最终计数
return 0;
}
atomic_flag
atomic_flag 是 C++11 引入的原子类型,专门用于实现简单的标志位操作。它是一个轻量级的原子标志,常用于锁的实现或其他需要线程间同步的场合。下面是对 atomic_flag 的详细解析。
-
基本特性
类型定义:std::atomic_flag 是一个类模板,表示一个原子标志。其大小通常是一个字节,但其具体实现依赖于编译器和平台。
原子操作:atomic_flag 提供了一组保证原子性的操作,可以在多线程环境中安全地访问和修改。
无状态:atomic_flag 只能取两个状态:设置(true)和未设置(false)。 -
主要操作
atomic_flag 提供了以下主要的操作:
test():检查标志的当前状态。如果标志被设置(true),返回 true,否则返回 false。
test_and_set():原子地设置标志为 true,并返回标志之前的状态。如果标志之前是未设置的(false),则它将返回 false;如果之前是设置的(true),则返回 true。这个操作通常用于实现自旋锁。
clear():原子地将标志清除为未设置状态(false)。这个操作通常用在锁的释放上。 -
示例代码
#include <iostream>
#include <thread>
#include <atomic>
std::atomic_flag flag;
void test_t1() {
while (flag.test_and_set(std::memory_order_acquire)) {
// 自旋等待,直到锁可用
}
std::cout << "12345" << std::endl; //操作1
std::cout << "67890" << std::endl; //操作2
flag.clear(std::memory_order_release);
}
int main() {
std::thread t1(test_t1);
std::thread t2(test_t1);
t1.join();
t2.join();
}
//如此则不会出现 12345 12345 67890 67890 这种顺序的输出了
四. condition_variable / condition_variable_any
#include <condition_variable> //头文件
std::condition_variable 是 C++11 引入的标准库中的同步原语之一,广泛用于线程间的通信和协调,特别是在多线程程序中,多个线程需要等待某个条件满足时,condition_variable 提供了一种高效的方式来实现线程的等待和通知机制。
- 基本概念
std::condition_variable 是一种线程间同步机制,它允许线程在某个条件满足时进行等待,直到其他线程发出通知信号。其主要用途是让一个线程在等待某个条件时释放互斥锁(mutex),并在条件满足时被其他线程唤醒。 - 主要操作
- 等待操作 (wait): 线程可以通过 condition_variable::wait 函数进入等待状态,直到某个条件满足。
- 通知操作 (notify_one 或 notify_all): 其他线程可以通过 notify_one 或 notify_all 来唤醒一个或所有正在等待的线程。
-
主要方法
wait wait_for wait_until notify_one() notify_all()
wait
//这个方法的基本原型如下:
wait(std::unique_lock<std::mutex>& lock)
//或者它的重载版本,可以接受一个条件谓词(Predicate):(推荐)
template< class Predicate >
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);
参数解释
std::unique_lock<std::mutex>& lock:
这是一个互斥量(mutex)的独占锁,它将在等待期间被自动释放。当线程进入等待状态时,std::unique_lock 会释放 mutex 锁,使得其他线程可以访问共享资源。
当 wait 方法返回时,std::unique_lock 会重新获取锁,从而保证线程的同步。
Predicate pred(在第二种重载方法中):
这是一个布尔条件谓词,通常是一个 lambda 函数或者其他可以返回 bool 的函数。
如果传递了 pred,线程会继续等待,直到 pred() 返回 true。每次被唤醒后,都会重新检查 pred() 的条件是否满足。
这通常用于避免“虚假唤醒”(spurious wakeups)。
虚假唤醒是指线程在没有调用 notify_one() 或 notify_all() 的情况下被唤醒。这种情况可能会发生,因此在使用 wait 时,最好总是将其放在一个 while 循环中检查条件,确保条件满足后再继续执行。
虚假唤醒通常是由于线程调度机制、操作系统实现或线程库底层的优化策略引起的,是一种不可预测的现象。
对于没有条件谓词的 wait 方法,while 循环通常是这样的:
std::unique_lock<std::mutex> lock(mtx);
while (!condition) {
cv.wait(lock); // 如果条件不满足,继续等待
}
使用带条件谓词的 wait 方法,while 循环会自动封装这个检查过程:
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return condition; }); // 只有条件为 true 时才会返回
一个典型的使用场景是生产者-消费者模型。以下是一个简单的例子:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
const unsigned int max_queue_size = 10;
void producer() {
for (int i = 0; i < 20; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return data_queue.size() < max_queue_size; }); // 当队列中有10个商品时,条件判断失败,wait生效,同时会解锁
data_queue.push(i);
std::cout << "Produced: " << i << std::endl;
lock.unlock(); //这里手动解锁,如果不手动解锁的话等待过程中消费者的wait依旧处于被锁住状态
cv.notify_all(); // 唤醒消费者
std::this_thread::sleep_for(std::chrono::milliseconds(100));//假设每生产一个商品需要等待100ms
}
}
void consumer() {
for (int i = 0; i < 20; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !data_queue.empty(); }); // 等待队列不为空
int data = data_queue.front();
data_queue.pop();
std::cout << "----Consumed: " << data << std::endl;
lock.unlock();
cv.notify_all(); // 唤醒生产者
std::this_thread::sleep_for(std::chrono::milliseconds(500)); //假设每消费一件商品需要等待500ms
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
wait_for
wait_for(std::unique_lock<std::mutex>& lock, const std::chrono::duration<Rep, Period>& rel_time)
除了等待条件变量外,还可以等待一定的时间,直到条件满足或时间到期。返回值为 std::cv_status,表示等待的状态(成功或超时)。
wait_until
wait_until(std::unique_lock<std::mutex>& lock, const std::chrono::time_point<Clock, Duration>& time_point)
等待直到某个时间点。类似于 wait_for,但使用绝对时间来指定超时。
notify_one
notify_one()
唤醒等待条件变量的其中一个线程。如果有多个线程在等待,只有其中一个线程会被唤醒。
notify_all
notify_all()
唤醒所有等待的线程。