博主自己学习的笔记,分享给大家,希望有用~
文章目录
- 一、基础使用
- 二、互斥量解决数据共享问题
- 三、条件变量<condition_variable>
- 四、原子操作 \<atomic>
- 五、创建后台任务
-
- `std::async` 详细讲解
- `std::shared_future`
- `packaged_task`
- `thread` 、`async`、 `packaged_task`对比
- promise
- 接口总结
- 六、线程池实现
一、基础使用
创建线程
#include <iostream>
#include <thread>
using namespace std;
void func() {
cout << "函数指针" << endl;
}
class MyFunc {
public:
void operator() () {
cout << "函数对象" << endl;
}
};
int main() {
//1.函数指针
thread tPtr(func);
//2.函数对象
thread tObj((MyFunc()));//注意要括号包裹起来
//3.Lambda表达式
thread tLambda([]() {cout << "Lamda表达式" << endl; });
tPtr.join();
tObj.join();
tLambda.join();
cout << "主线程退出" << endl;
return 0;
}
参数传递
1. 传值
- 线程函数 拷贝 传入的参数
- 适用于 基本数据类型 或者 小对象,不会影响原变量
- 避免数据竞争,但可能有性能开销
#include <iostream>
#include <thread>
void func(int x) {
x += 5;
std::cout << "Thread: " << x << std::endl;
}
int main() {
int a = 10;
std::thread t(func, a); // 传值
t.join();
std::cout << "Main: " << a << std::endl; // a 仍然是 10
}
三次构造
2. 1 传引用(std::ref)
- 让线程 操作原变量,不会拷贝
- 需要 保证变量在线程存活期间有效
- 可能引发 数据竞争,需要同步机制(如
mutex
)
#include <iostream>
#include <thread>
#include <functional>
void func(int& x) {
x += 5;
}
int main() {
int a = 10;
std::thread t(func, std::ref(a)); // 传引用
t.join();
std::cout << "Main: " << a << std::endl; // a 变成 15
}
一次构造
2.2 传递 const
引用
std::cref(a)
让x
变成const int&
,防止修改a
。- 适用于只读访问大对象,避免拷贝开销。
#include <iostream>
#include <thread>
void func(const int& x) {
std::cout << "Thread received (const ref): " << x << std::endl;
}
int main() {
int a = 10;
std::thread t(func, std::cref(a)); // 传 const 引用
t.join();
return 0;
}
两次构造
3. 传指针
- 线程函数接收 指针,可以修改原变量
- 需要确保变量不会被释放,否则可能出现悬垂指针
void func(int* x) {
*x += 5;
}
int main() {
int a = 10;
std::thread t(func, &a);
t.join();
std::cout << "Main: " << a << std::endl; // a 变 15
}
4. 右值引用(std::move)
- 线程函数 接管资源,主线程变量可能失效
- 适用于 大对象,防止不必要的拷贝
#include <iostream>
#include <thread>
#include <vector>
void func(std::vector<int> v) {
std::cout << "Thread received size: " << v.size() << std::endl;
}
int main() {
std::vector<int> vec = {1, 2, 3};
std::thread t(func, std::move(vec)); // 移动 vec
t.join();
std::cout << "Main vector size: " << vec.size() << std::endl; // 可能是 0
}
5. 使用 lambda 捕获
- 线程函数可以 直接访问外部变量
- 适用于 简单的逻辑,不需要额外的参数传递
int main() {
int a = 10;
std::thread t([&]() { a += 5; }); // 通过 [&] 传引用
t.join();
std::cout << "Main: " << a << std::endl; // a 变 15
}
6. 类成员函数作为线程函数
- 需要传递 对象指针,否则
this
指针丢失
class A {
public:
void func(int x) {
std::cout << "Thread: " << x << std::endl;
}
};
int main() {
A obj;
std::thread t(&A::func, &obj, 10);
t.join();
}
为什么 &
是必须的?
成员函数指针 和 普通函数指针 是不同的类型。在普通的函数指针中,指针可以直接指向函数。但是成员函数指针指向的是类的成员函数,而类的成员函数需要一个对象来调用。
&A::func
表示一个成员函数指针,它需要一个实例对象才能被调用。成员函数指针的语法规定在指针前面需要加一个&
符号(或者可以省略,但通常习惯上加上)。&
的作用:&A::func
是 成员函数指针,&
是指向成员函数的操作符。没有
&
的话,编译器无法正确识别这是一个成员函数指针,而会把它当成一个普通的函数指针,导致类型不匹配。
7. 传递临时对象
- 在主线程会构造一份,然后
#include <iostream>
#include <thread>
class A {
public:
A() { std::cout << "构造 A" << std::endl; }
A(const A&) { std::cout << "拷贝构造 A" << std::endl; }
~A() { std::cout << "析构 A" << std::endl; }
};
void func(A a) {}
int main() {
std::thread t(func, A()); // 传递临时对象
t.join();
}
8. 传递 std::move()
(移动对象)
std::move(vec)
让vec
失效(变为空),但避免了拷贝,提高效率。- 适用于传递大对象(如
std::vector
、std::string
)并避免拷贝。
#include <iostream>
#include <thread>
#include <vector>
void func(std::vector<int> v) {
std::cout << "Thread received vector of size: " << v.size() << std::endl;
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::thread t(func, std::move(vec)); // 移动 vec
t.join();
std::cout << "Main thread vector size: " << vec.size() << std::endl; // vec 变为空
return 0;
}
原文地址 www.seestudy.cn
线程函数中的数据未定义错误
1. 临时变量的问题
#include <iostream>
#include <thread>
void foo(int& x) {
x += 1;
}
int main() {
std::thread t(foo, 1); // 传递临时变量
t.join();
return 0;
}
在这个例子中,我们定义了一个名为 foo
的函数,它接受一个整数引用作为参数,并将该引用加 1。然后,我们创建了一个名为 t
的线程,将 foo
函数以及一个临时变量 1
作为参数传递给它。这样会导致在线程函数执行时,临时变量 1
被销毁,从而导致未定义行为。
解决方案是将变量复制到一个持久的对象中,然后将该对象传递给线程。例如,我们可以将 1
复制到一个 int
类型的变量中,然后将该变量的引用传递给线程。
#include <iostream>
#include <thread>
void foo(int& x) {
x += 1;
}
int main() {
int x = 1; // 将变量复制到一个持久的对象中
std::thread t(foo, std::ref(x)); // 将变量的引用传递给线程
t.join();
return 0;
}
2. 传递指针或引用指向局部变量的问题:
#include <iostream>
#include <thread>
void foo(int* ptr) {
std::cout << *ptr << std::endl; // 访问已经被销毁的指针
}
int main() {
int x = 1;
std::thread t(foo, &x); // 传递指向局部变量的指针
t.join();
return 0;
}
在这个例子中,我们定义了一个名为 foo
的函数,它接受一个整型指针作为参数,并输出该指针所指向的整数值。然后,我们创建了一个名为 t
的线程,将 foo
函数以及指向局部变量 x
的指针作为参数传递给它。这样会导致在线程函数执行时,指向局部变量 x
的指针已经被销毁,从而导致未定义行为。
解决方案是将指针或引用指向堆上的变量,或使用 std::shared_ptr
等智能指针来管理对象的生命周期。例如,我们可以使用 new
运算符在堆上分配一个整数变量,并将指针指向该变量。
#include <iostream>
#include <thread>
void foo(int* ptr) {
std::cout << *ptr << std::endl;
delete ptr; // 在使用完指针后,需要手动释放内存
}
int main() {
int* ptr = new int(1); // 在堆上分配一个整数变量
std::thread t(foo, ptr); // 将指针传递给线程
t.join();
return 0;
}
3. 传递指针或引用指向已释放的内存的问题:
#include <iostream>
#include <thread>
void foo(int& x) {
std::cout << x << std::endl; // 访问已经被释放的内存
}
int main() {
int* ptr = new int(1);
std::thread t(foo, *ptr); // 传递已经释放的内存
delete ptr;
t.join();
return 0;
}
在这个例子中,我们定义了一个名为 foo
的函数,它接受一个整数引用作为参数,并输出该引用的值。然后,我们创建了一个名为 t
的线程,将 foo
函数以及一个已经被释放的指针所指向的整数值作为参数传递给它解决方案是确保在线程函数执行期间,被传递的对象的生命周期是有效的。例如,在主线程中创建并初始化对象,然后将对象的引用传递给线程。
#include <iostream>
#include <thread>
void foo(int& x) {
std::cout << x << std::endl;
}
int main() {
int x = 1;
std::thread t(foo, std::ref(x)); // 将变量的引用传递给线程
t.join();
return 0;
}
在这个例子中,我们创建了一个名为 x
的整数变量,并初始化为 1
。然后,我们创建了一个名为 t
的线程,将 foo
函数以及变量 x
的引用作为参数传递给它。这样可以确保在线程函数执行期间,变量 x
的生命周期是有效的。
4. 类成员函数作为入口函数,类对象被提前释放
错误示例:
#include <iostream>
#include <thread>
class MyClass {
public:
void func() {
std::cout << "Thread " << std::this_thread::get_id()
<< " started" << std::endl;
// do some work
std::cout << "Thread " << std::this_thread::get_id()
<< " finished" << std::endl;
}
};
int main() {
MyClass obj;
std::thread t(&MyClass::func, &obj);
// obj 被提前销毁了,会导致未定义的行为
return 0;
}
上面的代码中,在创建线程之后,obj 对象立即被销毁了,这会导致在线程执行时无法访问 obj 对象,可能会导致程序崩溃或者产生未定义的行为。
为了避免这个问题,可以使用 std::shared_ptr 来管理类对象的生命周期,确保在线程执行期间对象不会被销毁。具体来说,可以在创建线程之前,将类对象的指针封装在一个 std::shared_ptr 对象中,并将其作为参数传递给线程。这样,在线程执行期间,即使类对象的所有者释放了其所有权,std::shared_ptr 仍然会保持对象的生命周期,直到线程结束。
以下是使用 std::shared_ptr 修复上面错误的示例:
#include <iostream>
#include <thread>
#include <memory>
class MyClass {
public:
void func() {
std::cout << "Thread " << std::this_thread::get_id()
<< " started" << std::endl;
// do some work
std::cout << "Thread " << std::this_thread::get_id()
<< " finished" << std::endl;
}
};
int main() {
std::shared_ptr<MyClass> obj = std::make_shared<MyClass>();
std::thread t(&MyClass::func, obj);
t.join();
return 0;
}
上面的代码中,使用 std::make_shared 创建了一个 MyClass 类对象,并将其封装在一个 std::shared_ptr 对象中。然后,将 std::shared_ptr 对象作为参数传递给线程。这样,在线程执行期间,即使 obj 对象的所有者释放了其所有权,std::shared_ptr 仍然会保持对象的生命周期,直到线程结束。
5. 入口函数为类的私有成员函数
#include <iostream>
#include <thread>
class MyClass {
private:
friend void myThreadFunc(MyClass* obj);
void privateFunc(){
std::cout << "Thread "
<< std::this_thread::get_id() << " privateFunc" << std::endl;
}
};
void myThreadFunc(MyClass* obj) {
obj->privateFunc();
}
int main() {
MyClass obj;
std::thread thread_1(myThreadFunc, &obj);
thread_1.join();
return 0;
}
上面的代码中,将 myThreadFunc
定义为 MyClass
类的友元函数,并在函数中调用 privateFunc
函数。在创建线程时,需要将类对象的指针作为参数传递给线程。
二、互斥量解决数据共享问题
互斥锁 mutex
Mutex 类型 | 描述 |
---|---|
std::mutex | 最基本的互斥锁类型,用于实现线程间的互斥访问。只允许一个线程获得锁,其他线程需要等待锁被释放才能继续执行。 |
std::recursive_mutex | 与 std::mutex 类似,但允许同一线程多次获取锁。也就是说,同一线程可以多次对该锁进行加锁操作,每次加锁都需要对应的解锁操作。 |
std::timed_mutex | 可限时等待的互斥锁类型。与 std::mutex 类似,但允许线程在尝试获取锁时设置一个超时时间。如果锁在指定的时间内无法被获得,线程将不再等待并返回相应的错误代码。 |
std::recursive_timed_mutex | 可限时等待的递归互斥锁类型。结合了 std::recursive_mutex 和 std::timed_mutex 的特性,允许同一线程多次获取锁,并且可以设置超时时间。 |
include<mutex>
mutex mtx;
mtx.lock();
mtx.unlock();
mtx.try_lock();
1. std::mutex (基本互斥锁)
std::mutex 是 C++ 标准库中提供的最基本的互斥锁类型之一。它用于实现线程间的互斥访问,即在一个时间点只允许一个线程获得锁,其他线程需要等待锁被释放才能继续执行。使用 std::mutex 可以保证多个线程对共享资源的访问顺序,并避免数据竞争产生的问题。
🚨注意:该类的对象之间不能拷贝,也不能进行移动。
⭕std::mutex 最常用的三个函数是:
函数名 | 描述 |
---|---|
lock() | 尝试获取互斥锁。如果未被其他线程占用,则当前线程获取锁;否则阻塞等待锁的释放。 |
unlock() | 释放互斥锁。如果当前线程持有锁,则释放锁;否则行为未定义。 |
try_lock() | 尝试获取互斥锁,不会阻塞线程。如果未被其他线程占用,则当前线程获取锁并返回 true;否则返回 false。 |
这三个函数组成了基本的互斥锁操作,也是使用 std::mutex 时最常用的三个函数。其中,lock() 和 unlock() 通常需要成对使用,以确保锁得到正确的管理。try_lock() 则可以用于一些特殊情况下的非阻塞式加锁操作,例如在轮询等待某个资源时,可以尝试获取锁并立即返回结果。
🚨注意事项
- 线程函数调用 lock() 时,可能会发生以下三种情况:
- 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock 之前,该线程一直拥有该锁。
- 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
- 如果当前互斥量被当前调用线程锁住,则会产生死锁 (deadlock)
- 线程函数调用 try_lock() 时,可能会发生以下三种情况:
- 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
- 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
- 如果当前互斥量被当前调用线程锁住,则会产生死锁 (deadlock)
2. std::recursive_mutex (递归互斥锁)
std::recursive_mutex 可以允许同一线程多次获取互斥锁,而不会导致死锁。简单来说就是允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock()
std::recursive_mutex
的主要方法和std::mutex
相同,包括lock()
、unlock()
和try_lock()
。
3. std::timed_mutex (限时等待互斥锁)
std::timed_mutex
在尝试获取锁的时候可以设置超时时间,避免线程由于无法获取锁而一直被阻塞等待,从而提高程序的健壮性。
std::timed_mutex
的主要方法和std::mutex
相同,包括lock()
、unlock()
和try_lock()
。不同的是,std::timed_mutex
提供了try_lock_for()
和try_lock_until()
这两个方法,用于在指定的时间范围内尝试获取互斥锁。
try_lock_for()
方法允许线程尝试在指定的时间段内获取互斥锁,如果在指定时间内无法获取锁,则返回 false。例如:
std::timed_mutex mtx;
std::chrono::milliseconds timeout(100);
if (mtx.try_lock_for(timeout))
{
// 成功获取锁
// ...
mtx.unlock(); // 释放锁
}
else
{
// 超时等待,未能获取锁
// ...
}
try_lock_until()
方法允许线程尝试在指定的时间点之前获取互斥锁,如果在指定时间点之前无法获取锁,则返回 false。例如:
std::timed_mutex mtx;
std::chrono::system_clock::time_point deadline = std::chrono::system_clock::now() + std::chrono::milliseconds(100);
if (mtx.try_lock_until(deadline))
{
// 成功获取锁
// ...
mtx.unlock(); // 释放锁
}
else
{
// 超时等待,未能获取锁
// ...
}
4. std::recursive_timed_mutex (限时等待递归互斥锁)
std::recursive_timed_mutex
是一个可递归、可超时等待的互斥锁类型
std::recursive_timed_mutex
的主要方法和std::timed_mutex
相同,包括lock()
、unlock()
和try_lock()
。不同的是,std::recursive_timed_mutex
允许同一线程多次获取锁,从而避免死锁等问题。
与std::timed_mutex
类似,std::recursive_timed_mutex
也提供了try_lock_for()
和try_lock_until()
方法,用于在指定的时间范围内尝试获取锁。
🚨注意:由于std::recursive_timed_mutex
允许同一线程多次获取锁,因此在释放锁之前,必须将锁计数器减少到零。否则,其他线程将无法获取到锁,从而导致死锁等问题。
std::lock_guard
1.1 特点
std::lock_guard
是一个轻量级的、简单的互斥量管理工具,它遵循 RAII(资源获取即初始化)原则。它的主要功能是自动加锁和自动解锁,并且在作用域结束时自动释放锁。
关键特点
自动加锁和自动解锁:在构造时会加锁,在析构时会自动解锁。无需手动调用加锁或解锁。
不可手动解锁:不能显式调用
unlock()
来解锁,解锁是在对象析构时发生的。不支持所有权转移:
std::lock_guard
不支持锁的所有权转移,也不能被复制或移动。因此,它是局部的、短生命周期的锁管理器。只能用于局部作用域:它是基于作用域的,在作用域结束时释放锁。
构造函数
std::lock_guard<mutex> lock_guard_instance(mutex_instance);
当 lock_guard_instance
被创建时,mutex_instance
会自动被加锁;当 lock_guard_instance
作用域结束时,mutex_instance
会自动解锁。
1.2 使用场景
简单的锁管理:当你只需要一个简单的锁,并且希望通过作用域管理锁时,
std::lock_guard
非常适用。例如,当你需要在一个函数中保护临界区资源时。防止死锁:由于它自动管理锁的生命周期,避免了在手动管理锁时可能出现的忘记解锁的情况,减少了死锁的风险。
示例
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx;
void print_hello() {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁
std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;
} // 在作用域结束时自动解锁
int main() {
std::thread t1(print_hello);
std::thread t2(print_hello);
t1.join();
t2.join();
return 0;
}
2. std::unique_lock
2.1 特点
std::unique_lock
是一个比 std::lock_guard
更加灵活的锁管理器,它支持手动解锁、重新加锁、所有权转移等操作。
关键特点
灵活的加锁和解锁:
std::unique_lock
允许手动解锁和重新加锁,可以在锁的生命周期内多次加锁和解锁。支持所有权转移:
std::unique_lock
支持锁的所有权转移,这使得它比std::lock_guard
更加灵活。例如,能够将一个unique_lock
的锁所有权传递给另一个对象。支持延迟加锁:通过构造时使用
defer_lock_t
参数,可以创建一个没有立即加锁的std::unique_lock
,可以稍后再加锁。支持
try_lock
、try_lock_for
和try_lock_until
:提供更多灵活的锁尝试机制,用于控制锁的获取方式,包括超时机制。与条件变量配合使用:
std::unique_lock
是与std::condition_variable
配合使用的标准选择,条件变量要求使用std::unique_lock
。
构造函数
std::unique_lock<mutex> unique_lock_instance(mutex_instance); // 自动加锁
std::unique_lock<mutex> unique_lock_instance(mutex_instance, std::defer_lock); // 延迟加锁
std::unique_lock<mutex> unique_lock_instance(mutex_instance, std::try_to_lock); // 尝试加锁
std::unique_lock<mutex> unique_lock_instance(mutex_instance, std::adopt_lock); // 假设锁已经被加锁
unique_lock() noexcept = default
:默认构造函数,创建一个未关联任何互斥量的std::unique_lock
对象。explicit unique_lock(mutex_type& m)
:构造函数,使用给定的互斥量m
进行初始化,并对该互斥量进行加锁操作。unique_lock(mutex_type& m, defer_lock_t) noexcept
:构造函数,使用给定的互斥量m
进行初始化,但不对该互斥量进行加锁操作。unique_lock(mutex_type& m, try_to_lock_t) noexcept
:构造函数,使用给定的互斥量m
进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的std::unique_lock
对象不与任何互斥量关联。unique_lock(mutex_type& m, adopt_lock_t) noexcept
:构造函数,使用给定的互斥量m
进行初始化,并假设该互斥量已经被当前线程成功加锁,相当于就是接管一下。
adopt_lock
表示这个互斥量已经被lock了;
std::adopt_lock标记的效果就是假设调用一方已经拥有了互斥量的所有权(已经lock成功了);通知lock_guard不需要再构造函数中lock这个互斥量了。
unique_lock也可以带std::adopt_lock标记,含义相同,就是不希望再unique_lock()的构造函数中lock这个mutex。
用std::adopt_lock的前提是,自己需要先把mutex lock上;用法与lock_guard相同。
try_to_lock
尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,并不会阻塞在那里;
用这个try_to_lock的前提是自己不能先lock
defer_lock
用std::defer_lock的前提是,你不能自己先lock,否则会报异常
std::defer_lock的意思就是并没有给mutex加锁:初始化了一个没有加锁的mutex
成员函数
lock()
:手动加锁,如果锁已经被其他线程持有,当前线程会被阻塞直到锁被成功获取。
void in()
{
for (int i = 0; i < 10000; i++)
{
cout << "in()执行,插入一个元素" << i << endl;
unique_lock<mutex> sbguard(my_mutex, defer_lock);//没有加锁的my_mutex
sbguard.lock();//咱们不用自己unlock
//处理共享代码
//因为有一些非共享代码要处理
sbguard.unlock();
//处理非共享代码要处理。。。
sbguard.lock();
//处理共享代码
msgRecvQueue.push_back(i);
//...
//其他处理代码
sbguard.unlock();//画蛇添足,但也可以
}
}
try_lock()
:尝试加锁,如果锁已经被其他线程持有,立即返回false
,否则加锁成功返回true
。
void in()
{
for (int i = 0; i < 10000; i++)
{
unique_lock<mutex> sbguard(my_mutex, defer_lock);//没有加锁的my_mutex
if (sbguard.try_lock() == true)//返回true表示拿到锁了
{
msgRecvQueue.push_back(i);
//...
//其他处理代码
}
else
{
//没拿到锁
cout << "inMsgRecvQueue()执行,但没拿到锁头,只能干点别的事" << i << endl;
}
}
}
try_lock_for()
:尝试在指定的时间内加锁,如果超时则返回false
。try_lock_until()
:尝试加锁,直到指定的时间点,如果超时则返回false
。unlock()
:手动解锁,必须显式调用。release()
:返回它所管理的mutex对象指针,并释放所有权
void in()
{
for (int i = 0; i < 10000; i++)
{
unique_lock<mutex> sbguard(my_mutex);
mutex *ptx = sbguard.release(); //现在你有责任自己解锁了,交接锁
msgRecvQueue.push_back(i);
ptx->unlock(); //自己负责mutex的unlock了
}
}
所有权传递
- unique_lock对象这个mutex的所有权是可以转移,但是不能复制。
std::unique_lock< std::mutex > sbguard1(my_mutex);
std::unique_lock< std::mutex > sbguard2(sbguard1);//此句是非法的,复制所有权是非法的
- 方法1 :std::move()
std::unique_lock<std::mutex> sbguard2(std::move(sbguard));//移动语义,现在先当与sbguard2与my_mutex绑定到一起了
//现在sbguard1指向空,sbguard2指向了my_mutex
- 方法2:return std:: unique_lock< std::mutex > 代码如下:
std::unique_lock<std::mutex> rtn_unique_lock()
{
std::unique_lock<std::mutex> tmpguard(my_mutex);
return tmpguard;//从函数中返回一个局部的unique_lock对象是可以的。三章十四节讲解过移动构造函数。
//返回这种举报对象tmpguard会导致系统生成临时unique_lock对象,并调用unique_lock的移动构造函数
}
void in()
{
for (int i = 0; i < 10000; i++)
{
std::unique_lock<std::mutex> sbguard1 = rtn_unique_lock();
msgRecvQueue.push_back(i);
}
}
2.2 使用场景
复杂的锁管理:当你需要对锁进行显式的加锁和解锁,或者需要在同一锁的生命周期内多次加锁和解锁时,
std::unique_lock
是最适合的选择。条件变量:
std::unique_lock
与std::condition_variable
一起使用时非常重要,因为条件变量要求锁必须是std::unique_lock
类型。延迟加锁:当你希望延迟加锁,或者希望在稍后的代码中显式加锁时,
std::unique_lock
提供了更大的灵活性。超时控制:通过
try_lock_for
和try_lock_until
,你可以控制在加锁时的等待时间,防止死锁。
示例
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx;
void print_hello() {
std::unique_lock<std::mutex> lock(mtx); // 自动加锁
std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;
lock.unlock(); // 手动解锁
std::cout << "Lock unlocked" << std::endl;
lock.lock(); // 重新加锁
std::cout << "Lock re-locked" << std::endl;
} // lock 在作用域结束时自动释放
int main() {
std::thread t1(print_hello);
std::thread t2(print_hello);
t1.join();
t2.join();
return 0;
}
特性 | std::lock_guard |
std::unique_lock |
---|---|---|
加锁方式 | 在构造时自动加锁 | 在构造时可以选择是否加锁,可以延迟加锁 |
解锁方式 | 在析构时自动解锁 | 可以显式调用 unlock() 进行解锁,也可以在作用域结束时自动解锁 |
灵活性 | 较低,仅支持自动加锁和自动解锁 | 较高,支持手动解锁、重新加锁、所有权转移 |
所有权转移 | 不支持 | 支持所有权转移 |
适用场景 | 简单的临界区保护,自动加锁和解锁 | 复杂的锁管理,适用于条件变量、延迟加锁、超时控制等 |
适配条件变量 | 不适合 | 必须使用 std::unique_lock 来配合条件变量 |
性能 | 较轻量 | 相对较重,但更灵活 |
三、条件变量<condition_variable>
C++11 条件变量(std::condition_variable
)详解
std::condition_variable
是 C++11 引入的一种同步原语,用于线程之间的通信和协调,尤其在多线程环境下,帮助线程管理特定条件的等待和通知。它是多线程编程中实现线程同步的重要工具,能够避免忙等待(busy-waiting),提高程序性能。
1. 条件变量的基本概念
std::condition_variable
提供了一种机制,允许线程在特定条件满足之前进入等待状态,等待的线程可以在条件满足后被唤醒。它通常与 std::mutex
或 std::unique_lock<std::mutex>
一起使用,确保线程在等待过程中能安全地访问共享资源。
常用操作
wait():线程调用
wait()
函数时,当前线程会被阻塞并等待条件变量满足特定条件。调用wait()
前,线程通常需要在互斥量(std::mutex
)的保护下检查某个条件是否为真。wait()
会释放互斥量,并将线程加入到条件变量的等待队列中。notify_one():通知至少一个正在等待条件变量的线程,通常会唤醒一个线程。如果有多个线程在等待,只有一个线程会被唤醒。
notify_all():通知所有等待条件变量的线程,使它们都可以被唤醒。
2. 代码示例
下面是一个典型的使用条件变量的生产者-消费者问题的示例:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
std::queue<int> q; // 用来模拟缓冲区
std::mutex mtx; // 用来保护缓冲区的互斥量
std::condition_variable cv; // 条件变量
// 生产者线程
void producer() {
for (int i = 0; i < 10; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产过程
std::lock_guard<std::mutex> lock(mtx); // 锁住互斥量
q.push(i);
std::cout << "生产者生产: " << i << std::endl;
cv.notify_all(); // 通知消费者
}
}
// 消费者线程
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx); // 获取互斥量
cv.wait(lock, [] { return !q.empty(); }); // 等待条件:缓冲区不为空
// 从队列中取出元素
int value = q.front();
q.pop();
std::cout << "消费者消费: " << value << std::endl;
// 退出条件:生产者已经生产完
if (value == 9) {
break;
}
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
3. 解释代码
3.1 生产者线程
生产者线程模拟生产过程,将数据 0
到 9
放入缓冲区(即队列 q
)。在每次生产数据后,生产者通过 cv.notify_all()
通知所有等待的消费者线程(如果有的话),唤醒它们去消费数据。
3.2 消费者线程
消费者线程首先通过 std::unique_lock
获取 mtx
互斥量,这样可以确保在访问共享数据时,其他线程无法同时访问该数据。然后,调用 cv.wait()
来等待条件变量。当队列 q
非空时,条件变量才会通知消费者线程继续执行。wait()
会自动释放互斥量,并使线程阻塞,直到条件满足。一旦条件满足,消费者就可以从队列中获取数据,并打印消费信息。当消费者消费到最后一个元素 9
时,退出循环。
好的!我来帮你补充一下关于条件变量相关函数的参数类型。
4. 条件变量的关键函数
4.1 wait()
cv.wait(lock);
参数类型:
lock
是一个std::unique_lock<std::mutex>
对象,表示对互斥锁的管理。lock
会在wait()
被调用时自动释放锁,并在条件变量被通知时重新获得锁。
返回值:
void
,表示线程进入等待状态,直到被唤醒。
4.2 wait()
带有条件
cv.wait(lock, condition);
参数类型:
lock
是一个std::unique_lock<std::mutex>
对象,表示对互斥锁的管理。condition
是一个可调用对象(如 lambda 函数),通常返回一个bool
值,表示条件是否满足。condition
会在调用wait()
时被反复检查,直到返回true
,才会继续执行。
返回值:
bool
,如果条件成立(condition
返回true
),则返回true
,表示线程继续执行;如果条件仍不成立,线程继续等待。
4.3 使用 wait()
的流程
cond.wait(lock);
当 wait()
没有第二个参数时,线程会进入阻塞状态,直到以下两种情况之一发生:
被其他线程唤醒:其他线程调用
notify_one()
或notify_all()
唤醒正在等待的线程。虚假唤醒:线程在没有收到
notify
的情况下被唤醒。
流程:
线程在调用
wait()
时会自动释放互斥锁,允许其他线程访问共享资源。线程会阻塞,直到满足以下条件之一:
其他线程调用
notify_one()
或notify_all()
唤醒它。虚假唤醒导致线程被唤醒。
线程在被唤醒后重新获取互斥锁,并继续执行。此时,线程没有条件判断,可能需要额外的检查来确认唤醒的时机是否正确。
缺点:
线程可能会由于虚假唤醒被唤醒,因此有可能继续执行不符合条件的逻辑。
没有第二个参数时,
wait()
不会自动检查共享资源的状态,可能导致错误的行为。
4.4 使用 wait()
带条件的流程
cond.wait(lock, [this]() { return !mes.empty(); });
当 wait()
有第二个参数时,第二个参数是一个可调用对象(如 lambda
函数),这个函数返回一个布尔值,表示条件是否满足。如果条件满足,wait()
会继续执行;如果条件不满足,线程会释放锁并进入阻塞状态,直到满足条件。
流程:
线程在调用
wait()
时,传入一个可调用对象(如lambda
)作为第二个参数。wait()
会检查该条件(lambda 表达式)。如果返回false
,则:- 线程会释放互斥锁并进入阻塞状态,等待其他线程调用
notify_one()
或notify_all()
唤醒它。
- 线程会释放互斥锁并进入阻塞状态,等待其他线程调用
如果返回
true
,或者在被唤醒后:- 线程会重新获取锁,并继续执行下去。
通过这种方式,即使线程被虚假唤醒,条件不满足时,线程会继续等待,直到条件为
true
,确保线程只在条件真正满足时才继续执行。
优点:
通过传入条件表达式,避免了虚假唤醒的影响。线程在醒来后会检查条件是否满足,只有条件满足时才会继续执行。
保证了线程在合适的时机继续执行,避免了无条件地继续执行可能导致的错误。
4.5小结
1. 有条件判断的情况:
条件判断允许线程在获得锁后检查是否满足继续执行的条件。如果条件满足,线程会继续执行;如果条件不满足,它会释放锁并进入等待队列。
条件变量在此情况下的作用是确保线程只有在合适的时机(即条件满足时)才会继续执行。
例如,在你给的代码中:
cv.wait(lock, []() { return turn == 0; });
这个判断会确保只有当
turn == 0
时,线程才会继续执行。否则,它会释放锁并进入等待队列,直到turn == 0
被设置为真,并且通知它的线程调用cv.notify_all()
唤醒它。这里的关键是线程只有在条件满足时才会继续执行,而不会因为没有条件判断而进入死锁状态。当一个线程满足条件时,它可以继续执行,而其他线程将会在条件未满足时保持阻塞。
2. 没有条件判断的情况:
当你没有设置条件判断时,线程在
cv.wait()
处会释放锁并进入等待队列。但是,如果没有线程唤醒它们,它们会永远被阻塞在等待队列中,无法继续执行。例如,下面这段代码:
cv.wait(lock);
如果没有任何线程调用
cv.notify_all()
或cv.notify_one()
,那么所有的线程都会处于等待状态,无法再执行后续代码。问题在于,如果你使用
cv.wait()
而没有条件判断,那么线程将会在等待队列中被阻塞,直到其他线程显式地调用notify_all()
或notify_one()
,否则它们永远不会被唤醒。
关键区别:
有条件判断时,线程会在等待队列中进入“阻塞状态”,但在条件满足时,线程会被唤醒并继续执行。
没有条件判断时,线程释放锁后进入等待队列,但除非外部线程调用
notify
,否则线程会永远处于阻塞状态。
4.3 notify_one()
cv.notify_one();
参数类型:
这个函数不需要任何参数。返回值:
void
,用于唤醒一个在等待的线程。
4.4 notify_all()
cv.notify_all();
参数类型:
这个函数不需要任何参数。返回值:
void
,用于唤醒所有在等待的线程。
4.5 std::condition_variable::wait_for()
cv.wait_for(lock, duration, condition);
参数类型:
lock
是一个std::unique_lock<std::mutex>
对象,表示对互斥锁的管理。duration
是一个时间持续量,可以使用std::chrono
库中的时间类型,例如std::chrono::seconds(1)
。condition
是一个可调用对象,通常返回一个bool
值,表示是否满足条件。
返回值:
bool
,表示是否在指定时间内满足条件。如果条件在指定时间内满足,则返回true
,否则返回false
。
4.6 std::condition_variable::wait_until()
cv.wait_until(lock, time_point, condition);
参数类型:
lock
是一个std::unique_lock<std::mutex>
对象,表示对互斥锁的管理。time_point
是一个时间点,通常是std::chrono::steady_clock::now() + duration
。condition
是一个可调用对象,通常返回一个bool
值,表示是否满足条件。
返回值:
bool
,表示是否在指定的时间点之前满足条件。如果条件在指定时间点之前满足,则返回true
,否则返回false
。
4.7 std::cv_status
枚举类型
用途:
std::cv_status
是wait_for()
和wait_until()
返回的状态值,用于表示是否超时。枚举类型:
cv_status::no_timeout
:表示等待超时前收到了通知。cv_status::timeout
:表示等待超时。
4.8 std::notify_all_at_thread_exit
std::notify_all_at_thread_exit(cv, std::move(lock));
参数类型:
cv
是一个std::condition_variable
对象,表示要通知的条件变量。lock
是一个std::unique_lock<std::mutex>
对象,用来管理对互斥锁的访问。这个锁会在调用线程退出时自动释放,并通知所有等待的线程。
返回值:
void
,调用该函数后,当前线程退出时会通知所有等待线程。
5. 注意事项
条件变量必须与互斥量结合使用:
std::condition_variable
本身不管理互斥量,它仅用于在线程间传递信号。因此,通常与std::mutex
或std::unique_lock<std::mutex>
一起使用。防止虚假唤醒:条件变量的
wait()
可能会因为虚假唤醒而提前返回。因此,在实际代码中通常使用如下形式的代码,确保条件始终为真:cv.wait(lock, []{ return condition_is_true; });
这样可以避免虚假唤醒带来的问题,确保在条件满足时才继续执行。
四、原子操作 <atomic>
C++ 原子操作的基本概念
在 C++ 中,原子操作的关键是 原子类型(atomic types)。原子类型是由 std::atomic
模板类提供的,它包装了一个类型,确保对该类型的所有操作(如读取、写入、更新等)都是原子的。
关键概念:
原子类型 (
std::atomic
):可以包装任何基本类型(如int
、bool
、float
等),并保证对这些类型的操作是原子的。原子操作:对原子变量的操作是不可分割的,意味着在多线程中不会被打断。
原子变量:一个变量可以被声明为原子类型(如
std::atomic<int>
),它保证在多线程环境下对该变量的操作是安全的。
常用的原子操作:
load:读取原子变量的值。
store:将一个值存储到原子变量中。
exchange:将原子变量的值替换为一个新值,并返回旧值。
compare_exchange_weak / compare_exchange_strong:原子地进行条件交换操作。若当前值等于预期值,则交换新值。
fetch_add / fetch_sub:原子地执行加法或减法操作,并返回旧值。
原子操作一般支持的操作包括:
- 基本算术操作:
++
,--
,+=
,-=
,&=
,|=
,^=
等。 - 这些操作是原子的,也就是说,当多个线程同时对
std::atomic<int>
进行增减或位操作时,编译器会确保每次操作是不可分割的。
为什么 g_count = g_count + 1;
不行:
g_count = g_count + 1;
这个表达式分为两步:- 读取
g_count
的值。 - 将计算出的新值写回
g_count
。
如果在这两步之间,另一个线程也读取并修改了
g_count
的值,最终可能导致结果错误。- 读取
原子操作(如
fetch_add
、fetch_sub
等)可以保证这类操作是 不可分割 的,避免中间被打断。通过原子操作,线程会依次执行加法或减法操作,确保每次修改都正确地反映到内存中。
std::atomic
类模板
std::atomic
是一个模板类,可以用于包装基础数据类型,确保对该数据类型的所有操作都是原子性的。
基本使用:
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
using namespace std;
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 100000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed); // 原子递增
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.push_back(std::thread(increment)); // 启动4个线程
}
for (auto& t : threads) {
t.join(); // 等待线程完成
}
cout << "Counter value: " << counter.load() << endl; // 输出最终结果
return 0;
}
常用原子操作的详细介绍:
1. load() 和 store()
load()
:从原子变量读取值。store()
:将值存储到原子变量中。
std::atomic<int> a(10);
int value = a.load(); // 获取原子变量a的值
a.store(20); // 将原子变量a的值设置为20
2. exchange()
exchange
会将原子变量的值更新为给定的值,并返回原先的值。常用来在并发环境下进行“交换”操作。
std::atomic<int> a(5);
int old_value = a.exchange(10); // 将a的值设置为10,并返回旧值5
3. fetch_add() 和 fetch_sub()
fetch_add()
:执行原子加法操作,并返回旧值。fetch_sub()
:执行原子减法操作,并返回旧值。
std::atomic<int> a(5);
int old_value = a.fetch_add(1); // 将a加1,并返回旧值
// a的值现在是6
4. compare_exchange_weak() 和 compare_exchange_strong()
这两个函数会比较当前原子变量的值和预期值。如果相同,就将原子变量的值设置为新值。否则,操作失败并返回 false
。
compare_exchange_weak()
:若失败,可能会重新尝试,性能相对较高。compare_exchange_strong()
:若失败,会返回false
,并且不再尝试。
std::atomic<int> a(5);
int expected = 5;
if (a.compare_exchange_weak(expected, 10)) {
// 如果a的值是5,设置为10
std::cout << "Value changed!" << std::endl;
} else {
std::cout << "Value not changed!" << std::endl;
}
5. memory_order
std::atomic
的操作可以指定不同的内存顺序(memory ordering),控制不同线程之间的操作顺序。这对于高效并发编程非常重要。常见的内存顺序有:
memory_order_relaxed
:不保证其他线程与该线程的操作顺序。memory_order_consume
:保证后续操作依赖于当前操作。memory_order_acquire
:保证所有的读取操作不会在当前操作之前执行。memory_order_release
:保证所有的写操作不会在当前操作之后执行。memory_order_acq_rel
:同时保证 acquire 和 release。memory_order_seq_cst
:最强的内存顺序,保证所有操作的顺序一致。
应用场景
计数器: 原子计数器是一个典型的应用场景。多个线程可能会并发修改一个计数器,原子操作可以保证计数器值的一致性。
锁的实现: 一些简化版的锁(如自旋锁)可以使用原子操作来减少性能开销。
无锁数据结构: 通过原子操作可以设计一些高效的无锁数据结构(如无锁队列、栈等)。
状态标志: 原子布尔值常常用来作为状态标志,例如线程是否完成,任务是否已开始等。
五、创建后台任务
std::async
详细讲解
std::async
是 C++11 引入的一个功能,它用于在后台异步执行一个函数并返回一个 std::future
对象,这样我们可以在未来某个时刻获取该任务的结果。它的核心作用是简化了多线程编程中的异步任务执行,避免了手动管理线程和线程同步的复杂性。
1. std::async
的基本原理
std::async
的作用是启动一个异步任务并返回一个 std::future
,我们可以通过 future
来获取任务的返回值。std::async
会根据其参数和系统的资源情况选择是创建一个新线程执行任务,还是延迟到调用 future::get()
或 future::wait()
时才执行任务。
2. std::async
的语法和参数
template <typename F, typename... Args>
std::future<typename std::result_of<F(Args...)>::type>
std::async(std::launch policy, F&& f, Args&&... args);
policy
: 任务启动的策略,它是一个std::launch
枚举类型,可以是:std::launch::deferred
: 延迟任务的执行,直到调用get()
或wait()
时才执行任务。std::launch::async
: 强制异步任务立即启动,在新线程中执行。std::launch::async | std::launch::deferred
: 系统根据资源来决定使用哪种方式。
f
: 传递给任务的可调用对象,通常是一个函数、Lambda 表达式、函数指针等。args
: 传递给函数的参数。
返回值是 std::future
,它可以用来获取任务的结果。
3. std::async
的工作机制
std::async
会根据提供的启动策略决定如何执行任务:
std::launch::async
强制
std::async
在一个新的线程中执行任务。任务一旦被提交,系统会立即分配新线程去执行。
std::launch::deferred
延迟执行任务,直到通过
future::get()
或future::wait()
请求结果时,才会执行任务。这意味着任务不会在调用
std::async
时立即开始执行,而是由调用者的线程在需要时(即等待结果时)执行任务。
s
td::launch::async | std::launch::deferred
由系统根据实际情况来决定采取哪种方案,资源充足async,不充足deferred。
不带额外参数 std::async(mythread),只给async 一个入口函数名,此时的系统给的默认值是 std::launch::async | std::launch::deferred。
4. 示例代码:使用 std::async
示例 1: 基本使用
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
using namespace std;
int task(int x) {
chrono::milliseconds dura(2000);
this_thread::sleep_for(dura);
cout << "Task finished: " << x << endl;
return x * 2;
}
int main() {
// 使用 std::async 启动一个异步任务
future<int> result = async(launch::async, task, 10);
cout << "Main thread continues..." << endl;
// 等待并获取任务结果
int res = result.get();
cout << "Task result: " << res << endl;
return 0;
}
输出:
Main thread continues...
Task finished: 10
Task result: 20
- 在此示例中,
std::async
会立即在一个新线程中启动task
函数,主线程继续执行,并最终通过result.get()
获取任务的返回值。
示例 2: 使用 std::launch::deferred
延迟执行
#include <iostream>
#include <future>
using namespace std;
int task(int x) {
cout << "Task executed with value: " << x << endl;
return x * 3;
}
int main() {
// 使用 std::launch::deferred 延迟执行任务
future<int> result = async(launch::deferred, task, 5);
cout << "Main thread continues..." << endl;
// 当调用 get() 时才执行任务
int res = result.get();
cout << "Task result: " << res << endl;
return 0;
}
输出:
Main thread continues...
Task executed with value: 5
Task result: 15
- 这里,任务的执行被延迟到调用
get()
时才会触发。主线程在此期间不会等待任务的执行,直到需要结果时,任务才会开始执行。
5. std::async
和 std::thread
的区别
特性 | std::async |
std::thread |
---|---|---|
线程管理 | 自动管理线程创建和销毁 | 需要手动创建线程并管理其生命周期 |
返回值获取 | 通过 std::future 获取返回值 |
需要手动传递回调或使用 std::promise |
异常处理 | 异常会被 std::future 捕获并在 get() 中抛出 |
需要显式捕获异常 |
线程执行时机 | 由 launch 策略决定(可延迟或立即执行) |
立即创建并执行 |
性能 | 如果系统资源紧张,可能不创建新线程,使用主线程执行任务 | 始终创建新线程 |
6. std::future
与 std::async
get()
: 阻塞调用,直到任务完成并返回结果。它会获取任务的返回值,如果任务异常终止,会在此抛出异常。wait()
: 阻塞调用,直到任务完成,但不会返回结果。通常用于等待任务执行完毕。都不调用的话,析构自动调用wait()
std::future_status status
机制:
卡住当前流程,等待std::async()的异步任务运行一段时间,然后返回其状态std::future_status。
如果std::async()的参数是std::launch::deferred(延迟执行),则不会卡住主流程。
std::future_status是枚举类型,表示异步任务的执行状态。类型的取值有
std::future_status::timeout
std::future_status::ready
std::future_status::deferred
示例 3: 使用 wait_for
检查任务状态
#include <iostream>
#include <future>
#include <chrono>
using namespace std;
int task() {
chrono::milliseconds dura(3000);
this_thread::sleep_for(dura);
return 100;
}
int main() {
future<int> result = async(launch::async, task);
std::future_status status = result.wait_for(std::chrono::seconds(2));
//std::future_status status = result.wait_for(6s);
if (status == std::future_status::timeout) {
//超时:表示线程还没有执行完
cout << "超时了,线程还没有执行完" << endl;
}
else if (status == std::future_status::ready) {
//表示线程成功返回
cout << "线程执行成功,返回" << endl;
cout << result.get() << endl;
}
else if (status == std::future_status::deferred) {
cout << "线程延迟执行" << endl;
cout << result.get() << endl;
}
return 0;
}
输出:
超时了,线程还没有执行完
wait_for
会在指定时间内等待任务的完成。如果任务未完成,会返回future_status::timeout
,如果任务完成,则返回future_status::ready
。
示例 4: 使用 wait_until
检查任务状态
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
int task() {
std::this_thread::sleep_for(std::chrono::seconds(3));
return 42;
}
int main() {
std::future<int> fut = std::async(std::launch::async, task);
// 获取当前时间
auto now = std::chrono::steady_clock::now();
// 设置截止时间为当前时间加3秒
auto timeout_time = now + std::chrono::seconds(2);
// 使用 wait_until 等待直到指定的时间点
if (fut.wait_until(timeout_time) == std::future_status::ready) {
std::cout << "Task completed within the time limit. Result: " << fut.get() << std::endl;
} else {
std::cout << "Task did not complete within the time limit!" << std::endl;
}
return 0;
}
7. 异常处理
在 std::async
中,如果异步任务抛出异常,异常会被保存在 std::future
对象中,并且在调用 get()
时重新抛出。因此,必须使用 try-catch
块来捕获这些异常。
示例 5: 异常处理
#include <iostream>
#include <future>
#include <stdexcept>
using namespace std;
int task() {
throw runtime_error("Something went wrong!");
return 42;
}
int main() {
future<int> result = async(launch::async, task);
try {
result.get(); // 会抛出异常
} catch (const exception& e) {
cout << "Caught exception: " << e.what() << endl;
}
return 0;
}
输出:
Caught exception: Something went wrong!
- 如果任务中抛出异常,它会被捕获并重新抛出,通过
future::get()
传递给调用者。
std::shared_future
与 std::future
类似,但它允许 多个线程共享同一个 future
,也就是说,多个线程可以获取同一个 shared_future
的结果。这使得在多线程环境中,可以更方便地处理异步任务的结果,避免重复计算。
在使用 std::future
时,一旦调用 get()
,它会使 future
对象变为“不可用”,即无法再调用 get()
,如果多个线程尝试获取同一个 future
的结果时,就会出现问题。而 std::shared_future
允许多个线程同时调用 get()
。
std::shared_future
与 std::future
区别
特性 | std::future |
std::shared_future |
---|---|---|
获取结果的方式 | 一次性获取,调用 get() 后不能再次获取 |
多次获取,多个线程可以调用 get() |
是否可以复制 | 不可以复制,移动语义 | 可以复制,支持多个线程共享同一个结果 |
线程安全 | 只能由一个线程获取结果 | 支持多个线程并发获取结果 |
1. 为什么需要 std::shared_future
?
多个线程共享结果:在一些情况下,你可能希望多个线程能够读取同一个任务的结果,而不是每个线程都要自己计算任务的结果。
std::shared_future
可以使得多个线程共享同一个结果,而不需要重新计算。避免重复计算:例如,你有一个复杂的计算任务,计算结果可以在多个地方使用,
std::shared_future
使得这些线程可以安全地共享结果,而不需要重新执行相同的计算。
2. 如何使用 std::shared_future
?
创建 shared_future
从
std::future
创建
我们可以先通过std::async
或其他方式获得一个std::future
对象,然后调用该std::future
对象的share()
方法来创建std::shared_future
。std::future<int> fut = std::async(std::launch::async, compute_result); std::shared_future<int> sharedFut = fut.share();
从
std::async
直接创建并转为shared_future
这里的“直接创建”其实指的就是第一种方式:先创建std::future
,然后通过share()
转换为std::shared_future
。std::shared_future<int> sharedFut = std::async(std::launch::async, compute_result).share();
示例 1:使用 std::future::share()
创建 shared_future
#include <iostream>
#include <future>
#include <thread>
void task() {
std::cout << "Task started in thread " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Task finished in thread " << std::this_thread::get_id() << std::endl;
}
int main() {
// 创建一个 std::future
std::future<void> fut = std::async(std::launch::async, task);
// 创建 std::shared_future,通过 share() 使得多个线程可以共享这个 future 的结果
std::shared_future<void> sharedFut = fut.share();
// 启动多个线程来获取结果
std::thread t1([&]() { sharedFut.get(); });
std::thread t2([&]() { sharedFut.get(); });
t1.join();
t2.join();
return 0;
}
在这个例子中,多个线程 t1
和 t2
都在获取相同的 shared_future
的结果。因为 std::shared_future
允许多个线程同时调用 get()
,所以它们可以安全地共享结果。
示例 2:std::shared_future
允许多个线程获取相同的结果
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
int task() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 42; // 返回计算结果
}
int main() {
std::future<int> fut = std::async(std::launch::async, task);
// 创建 shared_future
std::shared_future<int> sharedFut = fut.share();
// 启动多个线程来获取结果
std::thread t1([&]() {
std::cout << "Thread 1 got the result: " << sharedFut.get() << std::endl;
});
std::thread t2([&]() {
std::cout << "Thread 2 got the result: " << sharedFut.get() << std::endl;
});
t1.join();
t2.join();
return 0;
}
输出:
Thread 1 got the result: 42
Thread 2 got the result: 42
在这个例子中,两个线程 t1
和 t2
都能够通过 shared_future
获取相同的计算结果。每个线程都可以调用 get()
,而不会影响其他线程。
3. std::shared_future
和 std::future
的主要区别
多线程共享:
std::shared_future
允许多个线程调用get()
来获取结果,而std::future
只能由一个线程获取,且一旦get()
被调用,它就不再有效。复制性:
std::shared_future
是可复制的,你可以将它传递给多个线程,而std::future
是不可复制的,只能通过移动语义传递。线程安全:
std::shared_future
允许多个线程同时调用get()
,这是它最大的优势。而std::future
只能保证一次性获取结果。
4. shared_future
的常见用途
减少重复计算:当多个线程需要共享某个计算结果时,可以使用
std::shared_future
来避免重复计算。任务分发:当你有一个计算密集型的任务,计算结果可能在多个地方使用时,你可以使用
shared_future
让多个线程共享这个结果,而无需每次都重新计算。
示例:shared_future
在多个线程中共享状态
#include <iostream>
#include <future>
#include <thread>
#include <vector>
int compute_result() {
std::this_thread::sleep_for(std::chrono::seconds(2));
return 100;
}
int main() {
// 创建一个异步任务
std::future<int> fut = std::async(std::launch::async, compute_result);
// 创建 shared_future,使得多个线程可以共享计算结果
std::shared_future<int> sharedFut = fut.share();
// 启动多个线程来共享计算结果
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.push_back(std::thread([&sharedFut]() {
std::cout << "Thread " << std::this_thread::get_id() << " got result: " << sharedFut.get() << std::endl;
}));
}
// 等待所有线程执行完毕
for (auto& t : threads) {
t.join();
}
return 0;
}
packaged_task
目的:打包任务,把任务包装起来
- packaged_task是个模板类,它的模板参数是各种可调用对象
- std::packaged_task来把各种可调用对象包装起来,方便将来作为线程入口函数来调用
- packaged_task包装起来的可调用对象还可以直接调用,所以从这个角度来讲,packaged_task对象,也是一个可调用对象
std::packaged_task
本质上是用来将一个函数包装成一个任务,然后可以通过std::future
来获取该任务的执行结果。
1. packaged_task
的核心特性
包装可调用对象:它封装了一个可调用对象(如函数、lambda 或者函数对象),并允许将其作为线程任务传递给线程。
返回值与
std::future
配合:std::packaged_task
和std::future
紧密配合,packaged_task
用于启动任务,而future
用于获取任务的执行结果。线程同步:你可以通过
std::future
对象的get()
方法来获取任务执行的返回值,这会在任务完成时阻塞当前线程。
2. std::packaged_task
的工作流程
创建
packaged_task
:首先,你需要用一个可调用对象(例如函数、lambda 表达式等)来初始化std::packaged_task
。将
packaged_task
与线程关联:你可以将std::packaged_task
作为线程的任务来执行,packaged_task
会异步执行,并返回一个std::future
对象。通过
future
获取结果:通过std::future
对象的get()
方法来获取任务的执行结果。
3. packaged_task
常见用法
3.1 与 std::thread
配合使用
可以将 packaged_task
作为线程任务来执行,并通过 std::future
获取返回值
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
int compute_result(int x) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟长时间计算
return x * 2;
}
int main() {
// 创建一个 packaged_task,包装 compute_result 函数
std::packaged_task<int(int)> task(compute_result);
// 获取与 task 关联的 future 对象
std::future<int> result = task.get_future();
// 创建一个线程执行 task
std::thread t(std::move(task), 5);
// 等待线程执行并获取结果
std::cout << "Result: " << result.get() << std::endl;
// 等待线程完成
t.join();
return 0;
}
创建
packaged_task
:
我们创建了一个std::packaged_task<int(int)>
,它包装了一个返回int
类型值的函数compute_result
,这个函数接受一个int
类型的参数。获取
future
:
调用task.get_future()
来获取与packaged_task
相关联的std::future<int>
,该future
对象将用于获取任务执行的结果。启动线程:
我们通过创建一个线程,并将packaged_task
传递给线程执行。注意,必须将task
移动到线程中,因为std::packaged_task
是不可复制的,它只能被移动。获取结果:
通过result.get()
获取计算结果。如果线程没有完成,get()
会阻塞当前线程,直到任务完成并返回结果。等待线程结束:
最后,我们调用t.join()
等待线程完成。直接调用
packaged_task
:虽然packaged_task
主要是为了与线程配合使用,但它也可以直接调用,就像普通的可调用对象一样。
3.2 直接调用 std::packaged_task
#include <iostream>
#include <future>
#include <thread>
int compute_result(int x) {
std::this_thread::sleep_for(std::chrono::seconds(1));
return x * 10;
}
int main() {
// 创建一个 packaged_task,包装 compute_result 函数
std::packaged_task<int(int)> task(compute_result);
// 直接调用 packaged_task 对象,就像调用普通函数一样
int result = task(5); // 相当于调用 compute_result(5)
// 打印结果
std::cout << "Task result: " << result << std::endl;
// 还可以通过 get_future 来获取结果
std::future<int> fut = task.get_future();
// 注意:这里的 task 任务已经执行完毕,不会再有输出
std::cout << "Future result: " << fut.get() << std::endl;
return 0;
}
std::packaged_task<int(int)> task(compute_result);
这一行将compute_result
函数包装为一个std::packaged_task
对象。这个对象是一个可调用对象,它接受一个int
类型的参数并返回int
类型的结果。task(5);
这一行直接调用std::packaged_task
对象,它将执行compute_result(5)
,返回值会被存储在result
变量中。然后,
task.get_future()
获取与该任务关联的std::future
对象,可以用来异步获取结果。注意,task
被直接调用后,它的任务已被执行,因此fut.get()
会立即返回结果。
3.3 与容器配合使用
可以将多个 packaged_task
存放在容器中,并在多个线程中执行这些任务,最后获取每个任务的结果。
1. 基本使用
示例:使用 std::vector
存储多个 std::packaged_task
#include <iostream>
#include <future>
#include <thread>
#include <vector>
int compute_result(int x) {
std::this_thread::sleep_for(std::chrono::seconds(2));
return x * 10;
}
int main() {
std::vector<std::packaged_task<int(int)>> tasks;
std::vector<std::future<int>> futures;
// 创建多个 packaged_task 并存储到容器中
for (int i = 0; i < 3; ++i) {
tasks.push_back(std::packaged_task<int(int)>(compute_result));
futures.push_back(tasks.back().get_future()); // 获取每个任务的 future
}
// 使用 std::thread 来执行这些任务
std::vector<std::thread> threads;
for (int i = 0; i < tasks.size(); ++i) {
threads.push_back(std::thread(std::move(tasks[i]), i + 1)); // 移动任务到线程中
}
// 等待所有线程完成并输出结果
for (auto& t : threads) {
t.join();
}
for (auto& fut : futures) {
std::cout << "Task result: " << fut.get() << std::endl;
}
return 0;
}
在这个示例中,我们创建了一个
std::vector
来存储多个std::packaged_task
对象。每个
std::packaged_task
封装了一个计算任务compute_result
,并传递给多个线程来并行执行。通过
get_future()
获取每个任务的std::future
,从而可以获取到异步执行的结果。每个线程都通过
std::move()
来传递std::packaged_task
对象(因为std::packaged_task
是不可复制的)。
2. 批量管理任务
如果你希望批量处理任务而不关心每个任务的具体执行顺序,std::packaged_task
与容器的结合就显得特别有用。
例如,你可以将多个任务放入容器,然后通过线程池或其他并发机制来并行执行它们。在执行完毕后,你可以统一处理结果。
示例:使用 std::vector
批量执行任务并收集结果
#include <iostream>
#include <future>
#include <thread>
#include <vector>
int task(int x) {
std::this_thread::sleep_for(std::chrono::seconds(1));
return x * 2;
}
int main() {
std::vector<std::packaged_task<int(int)>> tasks;
std::vector<std::future<int>> results;
// 初始化任务并将它们存入容器
for (int i = 1; i <= 5; ++i) {
tasks.push_back(std::packaged_task<int(int)>(task));
}
// 启动线程并执行任务
for (size_t i = 0; i < tasks.size(); ++i) {
results.push_back(tasks[i].get_future()); // 获取 future
std::thread(std::move(tasks[i]), i + 1).detach(); // 执行任务
}
// 输出结果
for (auto& result : results) {
std::cout << "Task result: " << result.get() << std::endl;
}
return 0;
}
我们在
std::vector
中存储多个std::packaged_task
对象,每个任务都使用task
函数。通过
get_future()
获取每个任务的结果,并使用std::thread
启动异步执行。使用
detach()
启动的线程不需要join()
,但是需要确保线程的生命周期能够正确管理。最后通过
future.get()
获取每个任务的执行结果。
3. 与 std::async
配合
你也可以将 std::packaged_task
与 std::async
配合使用,创建异步任务。
示例:使用 std::async
配合 std::packaged_task
#include <iostream>
#include <future>
#include <vector>
int task(int x) {
std::this_thread::sleep_for(std::chrono::seconds(1));
return x * 3;
}
int main() {
std::vector<std::packaged_task<int(int)>> tasks;
std::vector<std::future<int>> futures;
// 初始化任务并将它们存入容器
for (int i = 1; i <= 5; ++i) {
tasks.push_back(std::packaged_task<int(int)>(task));
}
// 使用 std::async 启动任务并执行
for (size_t i = 0; i < tasks.size(); ++i) {
futures.push_back(std::async(std::launch::async, std::move(tasks[i]), i + 1)); // 执行任务
}
// 输出结果
for (auto& result : futures) {
std::cout << "Task result: " << result.get() << std::endl;
}
return 0;
}
在这个例子中,任务通过 std::async
异步执行,而不是使用 std::thread
。
std::packaged_task
的特殊之处
任务与
future
的绑定:std::packaged_task
将任务和std::future
紧密绑定。每个packaged_task
对象都有一个get_future()
方法,可以获取与任务执行结果相关的future
对象。这是std::thread
所没有的特性。std::async
也返回std::future
,但它自动启动线程并执行任务,而std::packaged_task
需要显式启动任务(如通过std::thread
或其他执行机制)。
手动控制任务执行:
std::thread
和std::async
都可以隐式启动线程并执行任务,而std::packaged_task
本身并不启动线程。你需要手动将packaged_task
传递给线程或者异步执行机制。这意味着你有更多的控制权,但同时也需要更多的代码来管理任务执行。
可重用性:
std::packaged_task
是可重用的。一个packaged_task
可以多次执行,只要它没有执行完毕。你可以通过不同的线程或任务来调用它。与之相比,std::thread
和std::async
每次只能执行一次任务。
thread
、async
、 packaged_task
对比
1. std::thread
std::thread
是 C++ 中最基础的并发工具,它提供了最基本的线程功能,可以用来在独立的线程中执行函数或可调用对象。
创建和管理线程:通过
std::thread
创建线程,线程会在后台运行指定的任务。没有与结果绑定:
std::thread
不能直接与std::future
绑定,因此它不提供返回值机制。如果你需要从线程中获取结果,通常需要使用共享变量、条件变量或其他同步机制。手动管理线程:你必须显式地使用
join()
或detach()
来管理线程生命周期。若忘记调用join()
或detach()
,会导致程序崩溃。
std::thread t([]() {
// 执行任务
});
t.join(); // 必须显式等待线程完成
主要特点:std::thread
是直接创建线程并执行任务,但不处理结果。
2. std::async
std::async
是 C++11 提供的高层次并发工具,它为你提供了一种简单的方式来启动异步任务,并通过 std::future
获取结果。
异步或同步执行:
std::async
可以选择异步执行(std::launch::async
)或同步执行(std::launch::deferred
)。异步模式下,它会启动一个独立线程执行任务,返回一个std::future
对象用于获取结果。同步模式下,只有在调用future.get()
时,才会真正执行任务。自动管理:
std::async
自动创建线程并管理任务的生命周期,你无需显式地管理线程的启动、等待或销毁。
std::future<int> fut = std::async(std::launch::async, compute_result);
std::cout << fut.get(); // 阻塞等待结果
主要特点:std::async
提供了简单的异步执行机制,自动管理线程和任务,并且能够通过 std::future
获取结果。
3. std::packaged_task
std::packaged_task
是 C++11 提供的另一个工具,它将一个可调用的任务封装成一个对象,允许通过 std::future
获取任务的执行结果。
封装任务:
std::packaged_task
将任务封装成一个可调用对象,使得它可以像普通函数那样调用,但它的特殊之处在于,任务的执行结果会通过std::future
来获取。与线程或
std::async
配合使用:std::packaged_task
通常与线程(或std::async
)结合使用,以启动任务并获取返回值。你可以手动创建线程执行packaged_task
,或者将它传递给std::async
。手动管理:与
std::async
自动创建线程不同,std::packaged_task
需要手动将任务与线程关联。也就是说,packaged_task
本身并不启动线程,你需要显式地将其传递给线程或其他执行机制。
thread示例
std::packaged_task<int()> task(compute_result);
std::future<int> fut = task.get_future();
std::thread t(std::move(task)); // 手动将任务传递给线程
t.join();
std::cout << fut.get(); // 阻塞等待结果
主要特点:std::packaged_task
是一个更低级的工具,允许你手动控制任务的执行。它本身不负责启动线程,而是与线程或其他机制配合使用。
async示例
使用 std::async
来执行 std::packaged_task
,通常的流程是:
创建一个
std::packaged_task
对象,它封装了一个可调用任务。通过
std::packaged_task::get_future()
获取一个std::future
对象,用来获取任务的结果。使用
std::async
启动异步执行,并将std::packaged_task
对象传递给std::async
,由std::async
来启动任务的执行。通过
std::future
获取执行结果。
#include <iostream>
#include <future>
#include <thread>
int compute_result() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
return 100;
}
int main() {
// 创建一个 std::packaged_task,封装任务
std::packaged_task<int()> task(compute_result);
// 获取一个 future 对象,用于获取任务执行结果
std::future<int> fut = task.get_future();
// 使用 std::async 启动任务,执行 packaged_task
std::async(std::launch::async, std::move(task)); // std::move 因为 task 是一个右值
// 等待并获取任务执行的结果
std::cout << "Result: " << fut.get() << std::endl;
return 0;
}
4. 总结
特性 | std::thread |
std::async |
std::packaged_task |
---|---|---|---|
任务执行方式 | 直接启动线程执行任务 | 自动启动线程或延迟执行任务 | 手动启动任务(通过线程等) |
结果获取 | 无内建机制获取结果 | 通过 std::future 获取结果 |
通过 std::future 获取结果 |
是否需要显式管理线程 | 需要显式调用 join() 或 detach() |
自动管理线程 | 需要显式将 packaged_task 与线程绑定 |
重用性 | 每个 std::thread 只能执行一次任务 |
每次调用会启动新的任务 | 可以多次执行相同的任务 |
灵活性 | 只支持线程执行任务 | 只能通过 async 执行任务 |
可以与任意执行机制配合使用(如线程、线程池等) |
promise
用于在一个线程中设置任务的结果,并允许另一个线程获取这个结果。std::promise
与 std::future
配合使用,通过 std::promise
设置结果,std::future
负责获取结果。
std::promise
:负责设置结果并提供一个std::future
对象。std::future
:负责获取结果,通常用于等待任务完成并返回结果。
函数方法
get_future()
:返回与std::promise
关联的std::future
对象。set_value(T&& value)
:用于设置结果,T
是任务的返回类型。set_exception(std::exception_ptr e)
:用于设置异常,允许任务抛出异常并通过std::future
传递给调用线程。std::promise
用于设置结果,std::future
用于获取结果。二者通过get_future()
和set_value()
/set_exception()
等方法进行交互。std::future
提供了get()
、wait()
等方法来等待任务结果。如果任务抛出异常,
std::promise
可以通过set_exception()
将异常传递给std::future
。
基础示例
以下是一个简单的例子,演示了如何使用 std::promise
和 std::future
进行线程间的结果传递
#include <iostream>
#include <thread>
#include <future>
// 任务函数,用于设置结果
void task(std::promise<int>&& prom) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟计算过程
prom.set_value(100); // 设置任务结果
}
int main() {
// 创建 std::promise 对象
std::promise<int> prom;
// 获取与 std::promise 关联的 std::future 对象
std::future<int> fut = prom.get_future();
// 启动一个线程执行任务,并传递 std::promise 对象
std::thread t(task, std::move(prom));
// 在主线程中等待结果并输出
std::cout << "Waiting for result..." << std::endl;
std::cout << "Result: " << fut.get() << std::endl;
// 等待线程完成
t.join();
return 0;
}
#include<iostream>
#include<future>
#include<thread>
using namespace std;
void mythread(promise<int>& tmp, int x) {
chrono::milliseconds dura(5000);
this_thread::sleep_for(dura);
tmp.set_value(x * 10); // 设置返回值
}
void mythread2(future<int>& tmp) {
auto result = tmp.get(); // 获取返回值
cout << "result = " << result << endl;
}
int main() {
cout << "主线程开始 id=" << this_thread::get_id() << endl;
promise<int> mypro;
thread t1(mythread, ref(mypro), 10);
t1.join();
future<int> fu1 = mypro.get_future();
thread t2(mythread2, ref(fu1));
t2.join();
cout << "主线程结束 id=" << this_thread::get_id() << endl;
return 0;
}
示例:传递异常
如果任务中发生异常,你可以通过 set_exception()
将异常传递给 std::future
。
#include <iostream>
#include <thread>
#include <future>
void task(std::promise<int>&& prom) {
try {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟计算过程
throw std::runtime_error("An error occurred"); // 模拟异常
} catch (...) {
prom.set_exception(std::current_exception()); // 传递异常
}
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(task, std::move(prom));
try {
fut.get(); // 获取任务结果,可能会抛出异常
} catch (const std::exception& e) {
std::cout << "Caught exception: " << e.what() << std::endl;
}
t.join();
return 0;
}
接口总结
1. std::future
std::future<T>
用于异步获取任务的结果,结果只能被获取一次。对象不可复制,但可移动。
关键特性:
get()
调用后,future
状态失效(valid()
返回false
)。- 析构时,若关联的异步结果未就绪且是最后一个引用,可能阻塞等待结果(取决于启动策略)。
常用成员函数:
get()
获取结果,调用后valid()
变为false
。若结果未就绪,阻塞当前线程。
若任务抛异常,get()
会重新抛出该异常。T get(); // T 可能为 void、引用或值类型
wait()
阻塞直到任务完成,不获取结果。void wait();
wait_for()
等待指定时间,返回状态future_status
(ready
、timeout
或deferred
)。template <class Rep, class Period> std::future_status wait_for(const std::chrono::duration<Rep, Period>& rel_time);
wait_until()
等待到指定时间点,返回状态future_status
。template <class Clock, class Duration> std::future_status wait_until(const std::chrono::time_point<Clock, Duration>& timeout_time);
valid()
检查是否关联有效结果(例如未调用get()
)。bool valid() const noexcept;
share()
转为std::shared_future<T>
,允许共享结果。std::shared_future<T> share();
2. std::shared_future
std::shared_future<T>
允许共享异步结果,可被多次调用 get()
。对象可复制。
关键特性:
get()
可多次调用,每次返回结果的副本(若T
非引用)。- 若
T
为引用类型(如T&
),需注意引用的有效性。
常用成员函数:
get()
返回结果副本(若T
非引用)。若T
为void
,无返回值。const T& get() const; // T 非 void 时的返回类型可能不同
wait()
/wait_for()
/wait_until()
同std::future
。valid()
同std::future
。
3. std::packaged_task
std::packaged_task<Function>
包装可调用对象(如函数、lambda),将其结果绑定到 std::future
。对象不可复制,但可移动。
关键特性:
- 调用
operator()
后,结果被存储到关联的future
中。 - 若多次调用
operator()
或未关联可调用对象,抛出std::future_error
。
常用成员函数:
构造函数
需提供可调用对象。默认构造的packaged_task
为空(valid() == false
)。explicit packaged_task(Function&& f);
operator()
执行任务并存储结果或异常。若已调用过或对象为空,抛出异常。void operator()(Args... args);
get_future()
返回关联的std::future<T>
。若已调用或对象为空,抛出异常。std::future<T> get_future();
swap()
交换两个packaged_task
的内容。void swap(packaged_task& other) noexcept;
reset()
(C++11 起)
重置任务,允许重新绑定新的可调用对象。void reset();
4. std::promise
std::promise<T>
用于显式设置异步结果或异常,关联的 std::future
可获取结果。对象不可复制,但可移动。
关键特性:
- 若
promise
析构前未设置结果,关联的future
会收到std::future_error
(broken_promise
)。 - 可设置结果或异常,但只能设置一次,重复设置抛出
std::future_error
。
常用成员函数:
构造函数
默认构造的promise
有效。移动构造函数允许转移所有权。promise(); promise(promise&& other) noexcept;
set_value()
设置结果。若T
为void
,无参调用。void set_value(const T& value); void set_value(T&& value); void set_value(); // 当 T 为 void 时
set_exception()
设置异常指针(如捕获的异常通过std::current_exception()
)。void set_exception(std::exception_ptr p);
set_value_at_thread_exit()
设置结果,但延迟到当前线程退出时通知future
。void set_value_at_thread_exit(const T& value);
set_exception_at_thread_exit()
类似set_value_at_thread_exit
,但设置异常。void set_exception_at_thread_exit(std::exception_ptr p);
get_future()
返回关联的std::future<T>
,只能调用一次。std::future<T> get_future();
swap()
交换两个promise
的内容。void swap(promise& other) noexcept;
总结对比
组件 | 核心功能 | 是否可复制 | 是否可移动 | 结果获取次数 | 特殊行为 |
---|---|---|---|---|---|
std::future |
单次获取异步结果 | 否 | 是 | 一次 | get() 后失效,析构可能阻塞 |
std::shared_future |
共享异步结果 | 是 | 是 | 多次 | get() 返回副本或引用 |
std::packaged_task |
包装可调用对象,绑定结果到 future |
否 | 是 | 一次 | 调用 operator() 后结果就绪 |
std::promise |
显式设置结果或异常 | 否 | 是 | 一次 | 未设置结果时析构会触发 broken_promise ,可设置延迟通知 |
关键注意事项
- 异常传递:所有组件的
get()
会传递任务中抛出的异常。 - 有效性检查:调用
get_future()
、get()
或set_value()
前需确保对象有效(如valid()
返回true
)。 - 线程安全:除
std::shared_future
的const
成员函数外,其他组件的方法通常不保证线程安全。 - 移动语义:
std::future
、std::packaged_task
和std::promise
支持移动语义,避免复制。 - 生命周期管理:确保
promise
或packaged_task
的生命周期长于关联的future
操作,防止悬空引用。
六、线程池实现
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <queue>
class ThreadPool {
public:
ThreadPool(int numThreads) : stop(false) {
for (int i = 0; i < numThreads; ++i) {
threads.emplace_back([this] {
while (true) {
std::unique_lock<std::mutex> lock(mutex);
condition.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) {
return;
}
std::function<void()> task(std::move(tasks.front()));
tasks.pop();
lock.unlock();
task();
}
});
}
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(mutex);
stop = true;
}
condition.notify_all();
for (std::thread& thread : threads) {
thread.join();
}
}
template<typename F, typename... Args>
void enqueue(F&& f, Args&&... args) {
std::function<void()> task(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
{
std::unique_lock<std::mutex> lock(mutex);
tasks.emplace(std::move(task));
}
condition.notify_one();
}
private:
std::vector<std::thread> threads;
std::queue<std::function<void()>> tasks;
std::mutex mutex;
std::condition_variable condition;
bool stop;
};
int main() {
ThreadPool pool(4);
for (int i = 0; i < 8; ++i) {
pool.enqueue([i] {
std::cout << "Task " << i << " is running in thread " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Task " << i << " is done" << std::endl;
});
}
return 0;
}