- thread、condition、mutex
- atomic
- function、bind
- 使用新特性实现线程池(支持可变参数列表)
- 异常
- 协程
1.1 线程thread
std::thread 在 #include 头文件中声明,因此使用 std::thread 时需要包含 #include 头文件。编译的时候后面也要链接上-lpthread
初始化构造函数
//创建std::thread执行对象,该thread对象可被joinable,新产生的线程会调用threadFun函数,该函
数的参数由 args 给出
template<class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args);
这里我们可以看到线程第一个参数是函数, 而函数的参数如果对应的是左值引用类型的话, 我们在传入后面几个参数的时候, 如果不表明ref强转为引用的话, 会导致后面的参数是值传递进来的, 并且对应的是右值, 而左值引用是不能绑定右值的, 即我们在传入线程参数过程中, 如果有引用尽量使用ref进行显式转换
默认删除拷贝构造
支持移动拷贝, 将线程的所有权转移至另一个句柄
主要成员函数
- get_id()
- 获取线程ID,返回类型std::thread::id对象。
- joinable()
- 判断线程是否可以加入等待
- join()
- 等该线程执行完成后才返回。
- detach()
- detach调用之后,目标线程就成为了守护线程,驻留后台运行,与之关联的std::thread对象
失去对目标线程的关联,无法再通过std::thread对象取得该线程的控制权。当线程主函数执
行完之后,线程就结束了,运行时库负责清理与该线程相关的资源。 - 调用 detach 函数之后:*this不再持有线程执行实例, joinable为false
例如:
- detach调用之后,目标线程就成为了守护线程,驻留后台运行,与之关联的std::thread对象
#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
void f1(int n)
{
for (int i = 0; i < 5; ++i) {
std::cout << "Thread 1 executing\n";
++n;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
void f2(int& n)
{
for (int i = 0; i < 5; ++i) {
std::cout << "Thread 2 executing\n";
++n;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
class foo
{
public:
void bar()
{
for (int i = 0; i < 5; ++i) {
std::cout << "Thread 3 executing\n";
++n;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int n = 0;
};
class baz
{
public:
void operator()()
{
for (int i = 0; i < 5; ++i) {
std::cout << "Thread 4 executing\n";
++n;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int n = 0;
};
int main()
{
int n = 0;
foo f;
baz b;
std::thread t1; // t1 不是线程
std::thread t2(f1, n + 1); // 按值传递
std::thread t3(f2, std::ref(n)); // 按引用传递
std::thread t4(std::move(t3)); // t4 现在运行 f2() 。 t3 不再是线程
std::thread t5(&foo::bar, &f); // t5 在对象 f 上运行 foo::bar()
std::thread t6(b); // t6 在对象 b 的副本上运行 baz::operator()
t2.join();
t4.join();
t5.join();
t6.join();
std::cout << "Final value of n is " << n << '\n';
std::cout << "Final value of f.n (foo::n) is " << f.n << '\n';
std::cout << "Final value of b.n (baz::n) is " << b.n << '\n';
}
互斥量
mutex又称互斥量,C++ 11中与 mutex相关的类(包括锁类型)和函数都声明在 头文件中,所以如果
你需要使用 std::mutex,就必须包含 头文件。
C++11提供如下4种语义的互斥量(mutex)
- std::mutex,独占的互斥量,不能递归使用。
- std::time_mutex,带超时的独占互斥量,不能递归使用。
- std::recursive_mutex,递归互斥量,不带超时功能。
- std::recursive_timed_mutex,带超时的递归互斥量。
unique_lock,lock_guard的区别
- unique_lock与lock_guard都能实现自动加锁和解锁,但是前者更加灵活,能实现更多的功能。
- unique_lock可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以进行解锁,
lck.lock()进行上锁,而不必等到析构时自动解锁。
条件变量的目的就是为了,在没有获得某种提醒时长时间休眠; 如果正常情况下, 我们需要一直循环
(+sleep), 这样的问题就是CPU消耗+时延问题,条件变量的意思是在cond.wait这里一直休眠直到
cond.notify_one唤醒才开始执行下一句; 还有cond.notify_all()接口用于唤醒所有等待的线程。
- wait: 如果线程被唤醒或者超时那么会先进行lock获取锁, 再判断条件(传入的参数)是否成立, 如果成立则wait函数返回否则释放锁继续休眠
- notify: 进行notify动作并不需要获取锁
条件变量
wait
template< class Lock, class Predicate >
void wait( Lock& lock, Predicate pred );
- 原子地解锁 lock ,阻塞当前执行线程,并将它添加到于 *this 上等待的线程列表。线程将在执行 notify_all() 或 notify_one() 时被解除阻塞。解阻塞时,无关乎原因, lock 再次锁定且 wait 退出。
- 等价于
while (!pred()) {
wait(lock);
}
- 可中断等待:在 wait() 期间注册 condition_variable_any ,使得若在给定的 stoken 的关联停止状态上作出停止请求,则提醒它;它等价于
while (!stoken.stop_requested()) {
if (pred()) return true;
wait(lock);
}
return pred();
注意返回值指示 pred 是否求值为 true ,无关乎是否作出了停止请求。
若这些函数不能满足后置条件(调用方线程锁定 lock ),则调用 std::terminate 。例如,这可能在重锁定互斥抛异常的情况下发生。
实例:
#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>
std::condition_variable_any cv;
std::mutex cv_m; // 此互斥用于三个目的:
// 1) 同步到 i 的访问
// 2) 同步到 std::cerr 的访问
// 3) 为条件变量 cv
int i = 0;
void waits()
{
std::unique_lock<std::mutex> lk(cv_m);
std::cerr << "Waiting... \n";
cv.wait(lk, []{return i == 1;});
std::cerr << "...finished waiting. i == 1\n";
}
void signals()
{
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lk(cv_m);
std::cerr << "Notifying...\n";
}
cv.notify_all();
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lk(cv_m);
i = 1;
std::cerr << "Notifying again...\n";
}
cv.notify_all();
}
int main()
{
std::thread t1(waits), t2(waits), t3(waits), t4(signals);
t1.join();
t2.join();
t3.join();
t4.join();
}
OPUTUT: ****
Waiting...
Waiting...
Waiting...
Notifying...
Notifying again...
...finished waiting. i == 1
...finished waiting. i == 1
...finished waiting. i == 1
包含两种重载,第一种只包含unique_lock对象,另外一个Predicate 对象(等待条件),这里必须使用
unique_lock,因为wait函数的工作原理:
- 当前线程调用wait()后将被阻塞并且函数会解锁互斥量,直到另外某个线程调用notify_one或者
notify_all唤醒当前线程;一旦当前线程获得通知(notify),wait()函数也是自动调用lock(),同理不能使用lock_guard对象。 - 如果wait没有第二个参数,第一次调用默认条件不成立,直接解锁互斥量并阻塞到本行,直到某一个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后无条件地继续进行后面的操作。
- 如果wait包含第二个参数,如果第二个参数不满足,那么wait将解锁互斥量并堵塞到本行,直到某一个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后继续判断第二个参数,如果表达式为false,wait对互斥量解锁,然后休眠,如果为true,则进行后面的操作。
wait_for函数
函数原型:
template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time);
template <class Rep, class Period, class Predicate>
bool wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time, Predicatepred);
和wait不同的是,wait_for可以执行一个时间段,在线程收到唤醒通知或者时间超时之前,该线程都会处于阻塞状态,如果收到唤醒通知或者时间超时,wait_for返回,剩下操作和wait类似。
wait_until
notify_one
notify_all
简单实例
#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>
std::condition_variable_any cv;
std::mutex cv_m;
int i = 0;
bool done = false;
void waits()
{
std::unique_lock<std::mutex> lk(cv_m);
std::cout << "Waiting... \n";
cv.wait(lk, []{return i == 1;});
std::cout << "...finished waiting. i == 1\n";
done = true;
}
void signals()
{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Notifying falsely...\n";
cv.notify_one(); // 等待线程被通知 i == 0.
// cv.wait 唤醒,检查 i ,再回到等待
std::unique_lock<std::mutex> lk(cv_m);
i = 1;
while (!done)
{
std::cout << "Notifying true change...\n";
lk.unlock();
cv.notify_one(); // 等待线程被通知 i == 1 , cv.wait 返回
std::this_thread::sleep_for(std::chrono::seconds(1));
lk.lock();
}
}
int main()
{
std::thread t1(waits), t2(signals);
t1.join();
t2.join();
}
可能的输出
Waiting...
Notifying falsely...
Notifying true change...
...finished waiting. i == 1
这里需要注意的是,wait函数中会释放mutex,而lock_guard这时还拥有mutex,它只会在出了作用域之后才会释放mutex,所以这时它并不会释放,但执行wait时会提前释放mutex。
从语义上看这里使用lock_guard会产生矛盾,但是实际上并不会出问题,因为wait提前释放锁之后会处于等待状态,在被notify_one或者notify_all唤醒后会先获取mutex,这相当于lock_guard的mutex在释放之后又获取到了,因此,在出了作用域之后lock_guard自动释放mutex不会有问题。
这里应该用unique_lock,因为unique_lock不像lock_guard一样只能在析构时才释放锁,它可以随时释放锁,因此在wait时让unique_lock释放锁从语义上更加准确。
原子变量
定义于头文件 <memory>
注意:
尽量不要进行简单的赋值初始化
atomic<int> flag = 1; // 不建议如此操作 错误初始化
atomic<int> flag(1); // 建议如此操作 准确初始化
operator = 等价于 store
另外再C++20中atomic还拥有wait和nofity_one函数已经原子指针
定义于头文件 <stdatomic.h>
一定条件下可以使用std::atomic<std::shared_ptr>作为原子共享指针来进行编写代码
wait(C++20)
阻塞线程直至被提醒且原子值更改
notify_one(C++20)
提醒至少一个在原子对象上的等待中阻塞的线程
异步操作
- std::future : 异步指向某个任务,然后通过future特性去获取任务函数的返回结果。
- std::aysnc: 异步运行某个任务函数
- std::packaged_task :将任务和feature绑定在一起的模板,是一种封装对任务的封装。
- std::promise