C++:线程库
thread
操作线程需要头文件<thread>
,头文件包含线程相关操作,内含两个内容:
thread
类:操作线程的基本类this_thread
命名空间域:用于操作当前线程
thread
thread
是用于操作线程的类,其实现了对多平台线程的封装,以统一的面向对象方式完成线程的操作。
thread
构造:
cpp
thread() noexcept;
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
thread (const thread&) = delete;
thread (thread&& x) noexcept;
从以上构造的声明可以得出以下要点:
- 可以不传参,直接构造一个空线程对象
- 禁止拷贝构造
- 支持移动构造
其中第二个声明是最重要的:
cpp
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
这是一个函数模板,参数如下:
fn
:一个可调用对象,创建线程后线程执行该函数内容args
:可变参数包,用于给fn
传参,可以传任意数量的参数
示例:
cpp
#include <iostream>
#include <thread>
void test(int x, int y)
{
std::cout << x + y << std::endl;
}
int main()
{
int a = 3;
int b = 5;
std::thread t(test, a, b);
return 0;
}
这就创建了一个线程对象,并且设定好了该线程要执行的函数。
此处最大的便利在于,不论是Windows
还是Linux
,创建线程时传入的函数,都只允许传入一个参数,而此处可以通过参数包传入任意数量的参数。
- 成员函数:
operator=
thread
线程类的赋值重载,同样禁止拷贝赋值,只允许移动赋值。
还记得我们可以创建一个空线程类吗,没有函数可以给一个空线程类绑定函数,只能通过移动赋值来完成初始化空线程类。
示例:
cpp
#include <iostream>
#include <vector>
#include <thread>
void test(int x, int y)
{
std::cout << x + y << std::endl;
}
int main()
{
std::vector<std::thread> v(100);
for (auto& th : v)
{
th = std::thread(test, 100, 200);
}
return 0;
}
当要管理多个线程的时候,可以把多个线程对象放到一个容器内部统一管理,此处用一个vector
管理了100个线程对象。但是一开始可能还不能确定线程对象要执行哪一个函数,所以会在容器内部构建空线程对象。需要初始化时,就得用一个匿名对象通过构造函数创建非空线程对象,随后通过operator=
完成移动赋值。
以上示例中, th = std::thread(test, 100, 200)
被放在for
循环内部,完成对所有空线程对象的初始化。
get_id
直接获取一个线程的id
,其声明如下:
cpp
id get_id() const noexcept;
如果仔细观察返回值,你会发现其返回的不是一个整数,而是一个叫做id
的类型。其是thread
的内部类,由于不同操作系统对线程的标识符不同,比如Linux
使用整数tid
标识,而Windows
使用线程句柄标识不同线程。所以无法统一线程的id,因此C++将不同操作系统的标识符封装为一个类thread::id
。
thread::id
重载了以下操作符:
cpp
bool operator== (thread::id lhs, thread::id rhs) noexcept;
bool operator!= (thread::id lhs, thread::id rhs) noexcept;
bool operator< (thread::id lhs, thread::id rhs) noexcept;
bool operator<= (thread::id lhs, thread::id rhs) noexcept;
bool operator> (thread::id lhs, thread::id rhs) noexcept;
bool operator>= (thread::id lhs, thread::id rhs) noexcept;
允许进行比大小,判断相等的操作,因此可以放在容器中作为键值,用于管理线程比如map
、set
之类的容器。
那么能否作用于unordered_map
这样的哈希容器呢?想要将数据放到哈希表中作为键,就需要一个哈希函数将数据转化为一个整数下标。而id
是一个复杂的类,不同系统下内部的内容不一样,很难写出一个统一的哈希函数完成转化。而C++对此进行了模板特化,在thread::id
作为哈希的键时,C++内部实现了哈希函数,所以thread::id
可以直接在哈希表内部使用。
cpp
template <class T> struct hash; // 通用模板声明
template<> struct hash<thread::id>; // 对 thread::id 的模板特化
另外的,thread::id
还支持流输出:
cpp
template <class charT, class traits>
basic_ostream<chasrT, traits>& operator<< (basic_ostream<charT,traits>& os, thread::id id);
可以直接用cout
这样的流输出对象来输出id
:
cpp
std::thread th(test, 100, 500);
std::cout << th.get_id() << std::endl;
join
用于回收线程,调用该函数后,主线程进入阻塞,直到被join
的线程结束,最后回收该线程。
示例:
cpp
#include <iostream>
#include <vector>
#include <thread>
void test(int x, int y)
{
std::cout << x + y << std::endl;
}
int main()
{
std::thread th(test, 100, 500);
th.join();
return 0;
}
只需要线程对象.join()
就可以完成线程资源的回收,还是比较方便的。
detatch
用于线程分离,被分离的线程将自己回收自己,无需再join
。
cpp
#include <iostream>
#include <vector>
#include <thread>
void test(int x, int y)
{
std::cout << x + y << std::endl;
}
int main()
{
std::thread th(test, 100, 500);
th.detach();
return 0;
}
但是此处你很有可能会看不到线程的输出结果,因为线程th
被分离后,就无需主线程回收了,主线程直接return
,线程结束。但是由于主线程退出,同一进程下的所有线程也会推出,导致线程th
还没有来得及输出,就被强制退出了。
joinable
用于检测一个线程是否允许被join
,如果线程被detach
或已经被join
了,那么joinable
就会返回false
,反之返回true
。
this_thread
std::this_thread
是一个命名空间,用于访问当前线程。它提供了一组函数来操作当前线程。
get_id()
: 返回当前线程的 ID。yield()
: 让出 CPU 时间片,让其他线程运行。sleep_until()
: 使当前线程睡眠直到某个时间点。sleep_for()
: 使当前线程睡眠一段时间。
get_id
和yield
都可以直接执行,不用传入参数。而后两个函数与时间相关,要用到C++封装的时间chrono
。
chrono
<chrono>
是一个头文件,内包含chrono
命名空间域,该域内部封装了各种时间的相关操作。
- 时钟
clock
std::chrono::system_clock
:系统时钟,表示从 Unix 纪元开始的时间(1970 年 1 月 1 日 00:00:00 UTC)。std::chrono::steady_clock
:稳定时钟,表示从程序启动开始的时间。
这两个时钟都有一个now
成员函数,返回当前的时间。但是system_clock
会受到系统时钟影响,如果用户调整了系统时间,就有可能造成时间错误,而稳定时钟不受系统时钟影响。
cpp
auto t1 = std::chrono::system_clock::now();
auto t2 = std::chrono::steady_clock::now();
这两个函数都返回一个time_point
类型,表示当前时间点。
- 时间段
duration
duration
用于表示一个时间段,这个类的用法比较复杂,因此C++为我们封装了一些可以直接使用的类:
std::chrono::nanoseconds
(纳秒)std::chrono::microseconds
(微秒)std::chrono::milliseconds
(毫秒)std::chrono::seconds
(秒)std::chrono::minutes
(分钟)std::chrono::hours
(小时)
这些类都是typedef
后的duration
,如果想要表示一个时间端,直接传数字即可:
cpp
auto dur1 = std::chrono::seconds(3); // 3秒
auto dur2 = std::chrono::minutes(5); // 5分钟
- 时间点
time_point
time_point
表示一个时间点,之前时钟返回的now
,就是当前的时间点。C++支持了以下运算:
- 时间点
time_point
+ 时间段duration
= 时间点time_point
- 时间段
duration
- 时间段duration
= 时间段duration
- 时间点
time_point
- 时间点time_point
= 时间段duration
关于时间类,就简单了解到这里,此处只讲解了最基础的概念,为了讲解线程库的相关接口。
回到this_thread
内部的函数:
sleep_until()
: 使当前线程睡眠直到某个时间点。sleep_for()
: 使当前线程睡眠一段时间。
sleep_until
需要传入一个时间点time_point
,比如想要睡眠10
秒,就可以用当前时间 + 10秒
得到一个时间点,再用sleep_until
完成睡眠:
cpp
auto t1 = std::chrono::steady_clock::now(); // 获取当前时间
auto dur = std::chrono::seconds(10); // 获取十秒时间段
auto t2 = t1 + dur; // 时间点 + 时间段 = 时间点,十秒后
std::this_thread::sleep_until(t2); // 睡眠到 10 秒后
sleep_for
需要传入一个时间段duration
,同样的睡眠十秒:
cpp
auto dur = std::chrono::seconds(10); // 获取十秒时间段
std::this_thread::sleep_for(dur); // 睡眠到 10 秒后
引用拷贝问题
再看thread
类的声明:
cpp
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
此处可以发现,其构造参数使用的是&&
引用折叠,可以传左值/右值引用。
尝试一下:
cpp
void test(int& x, int& y)
{
x += 100;
y += 200;
}
int main()
{
int a = 30;
int b = 50;
std::thread t1(test, a, b);
t1.join();
std::cout << "a = " << a << std::endl;
std::cout << "b = " << b << std::endl;
return 0;
}
这段代码无法正常运行,因为线程传参时,无法直接传引用,为什么?
不论是Linux
还是Windows
系统,创建多线程时,函数都只允许传入一个参数,比如Linux
只允许传一个void*
的变量。
但是C++封装后,允许传入多个值,最终一定要把这多个参数和函数封装在一个类内部,成为一个可调用对象(仿函数),一起通过一个变量传给线程函数。
而C++为了确保线程拿到的参数是有效的,不会出现线程拿到参数后,参数被主线程销毁了等情况。不论是普通变量,引用还是指针,都会进行一次拷贝。
引用一旦经过拷贝,拷贝后的变量和拷贝前的变量,就不是一个变量了。所以此处传入引用会报错,在线程传参时,不能直接传引用。
但是指针不怕拷贝,指针拷贝后,依然指向原先的变量。该问题的解决策略有三个:
- 使用指针进行传址调用
- 使用引用包装器
- 使用lambda传引用
引用包装器:
引用包装器用于解决引用的拷贝问题,引用拷贝后依然是原先的变量。
引用包装器的原理如下:
cpp
template <typename T>
class reference_wrapper
{
public:
// 构造函数,接受一个引用
explicit reference_wrapper(T& ref)
: _ptr(&ref)
{}
// 拷贝构造
reference_wrapper(const reference_wrapper& other)
: _ptr(other._ptr)
{}
operator T& () const
{
return *_ptr;
}
private:
T* _ptr;
};
引用包装器内部存储一个指针,当包装引用时,_ptr
成员会存储引用的指针。当拷贝时,也是对指针进行拷贝,指针拷贝后,依然指向原先的变量。
最核心的是重载了operator T&
,也就是隐式类型转化,此时引用包装器可以转化为一个T&
引用,所以引用包装器可以当作引用来使用。
如果想要将一个引用包装起来,可以使用std::ref()
函数,其返回一个引用的引用包装器。另外的,如果是const
引用,则使用std::cref()
。
对于刚才的线程问题,只需要在传递参数时用包装器包装一层即可:
cpp
std::thread t1(test, std::ref(a), std::ref(b));
结合lambda:
除了以上两种方式,也可以直接使用lambda
的捕捉列表,以引用的形式捕捉变量:
cpp
int main()
{
int a = 30;
int b = 50;
auto func = [&a, &b]() {
a += 100;
b += 200;
};
std::thread t1(func);
t1.join();
std::cout << "a = " << a << std::endl;
std::cout << "b = " << b << std::endl;
return 0;
}
lambda
的直接捕捉,不用通过std::thread
进行参数传递,所以就不会出现引用拷贝的问题。
mutex
既然要进行多线程并发编程,自然少不了线程安全的问题,<mutex>
头文件内部,封装了各种锁,用于维护线程安全。
头文件内包含四种锁:
mutex
:互斥锁recursive_muetx
:递归锁timed_mutex
:时间锁recursive_timed_muetx
:时间递归锁
以及两种基于ARII的加锁策略:
lock_guard
:作用域锁unique_lock
:独占锁
mutex
mutex
是最基础的互斥锁,可以对资源进行加锁解锁。
成员函数:
lock
:加锁unlock
:解锁try_lock
:如果没上锁就加锁,上锁了就返回
可以看出,mutex
的使用非常简单。
示例:
cpp
int num = 0;
void test(int n)
{
for (int i = 0; i < n; i++)
{
num++;
}
}
int main()
{
std::mutex mtx;
std::thread t1(test, 2000);
std::thread t2(test, 2000);
t1.join();
t2.join();
std::cout << "num = " << num << std::endl;
return 0;
}
该代码中,两个线程一起对同一个num++
,每个线程2000
次,但是最后输出的num
很有可能比2000
少,因为num++
不是原子性的。此时对其加锁:
cpp
int num = 0;
void test(int n, std::mutex& mtx)
{
for (int i = 0; i < n; i++)
{
mtx.lock();
num++;
mtx.unlock();
}
}
int main()
{
std::mutex mtx;
std::thread t1(test, 2000, std::ref(mtx));
std::thread t2(test, 2000, std::ref(mtx));
t1.join();
t2.join();
std::cout << "num = " << num << std::endl;
return 0;
}
通过std::mutex mtx
定义了一个名为mtx
的锁,随后通过std::thread
将锁作为参数传入线程,在每次num++
前加锁,后释放锁。
timed_mutex
时间锁就是限定每次申请锁的时长,如果超过一定时间没有申请到锁,就返回。
成员函数:
lock
:加锁unlock
:解锁try_lock
:如果没上锁就加锁,上锁了就返回try_lock_until
:如果到指定时间还没申请到锁就返回false
,申请到锁返回true
try_lock_for
:如果一段时间内没申请到锁就返回false
,申请到锁返回true
通过之前的经验,可以猜出try_lock_until
要传入一个time_point
时间点,而try_lock_for
要传入一个时间段duration
。
以try_lock_for
为例:
cpp
int num = 0;
void test(int n, std::timed_mutex& mtx)
{
while (n)
{
bool ret = mtx.try_lock_for(std::chrono::microseconds(1));
if (ret)
{
num++;
n--;
mtx.unlock();
}
else
{
std::cout << "加锁超时" << std::endl;
}
}
}
int main()
{
std::timed_mutex mtx;
std::thread t1(test, 2000, std::ref(mtx));
std::thread t2(test, 2000, std::ref(mtx));
t1.join();
t2.join();
std::cout << "num = " << num << std::endl;
return 0;
}
此处每次申请锁不超过一微秒,如果try_lock_for
返回true
说明抢到锁了,进行num++
,反之则输出加锁超时
。
recursive_mutex
递归锁用于解决函数的递归造成的死锁,比如这样:
cpp
int num = 0;
void test(int n, std::mutex& mtx)
{
if (n <= 0)
return;
mtx.lock();
test(n - 1, mtx);
mtx.unlock();
}
int main()
{
std::mutex mtx;
std::thread t1(test, 2000, std::ref(mtx));
t1.join();
std::cout << "num = " << num << std::endl;
return 0;
}
函数test
中会产生死锁,第一次递归对mtx
加锁,第二次递归时由于自己已经占有锁了,再次申请锁就会阻塞,导致死锁。
递归锁就是用于解决这样的自己与自己造成的死锁局面。
cpp
int num = 0;
void test(int n, std::recursive_mutex& mtx)
{
if (n <= 0)
return;
mtx.lock();
test(n - 1, mtx);
mtx.unlock();
}
int main()
{
std::recursive_mutexmtx;
std::thread t1(test, 2000, std::ref(mtx));
t1.join();
std::cout << "num = " << num << std::endl;
return 0;
}
recursive_mutex
与mutex
的用法完全一致,以上代码中只需要把mutex
换为recursive_mutex
就可以避免死锁。
因为mutex.lock()
时,如果申请不到锁,不论是谁占有这把锁,都会陷入阻塞,直到锁被释放。recursive_mutex
则会记录是谁占有这把锁,在recursive_mutex.lock()
时,会检查申请锁的线程和占有锁的线程是不是同一个,如果是同一个,则直接申请成功,因此可以避免递归死锁。
lock_guard
在使用锁的过程中,最忌讳的就是忘记解锁,这就会导致一个线程一直持有锁,其他线程无法访问到资源。但是难道每次用完锁后解锁,就可以保证锁被释放吗?
看到这个例子:
cpp
void test(int n, std::mutex& mtx)
{
mtx.lock();
// 抛异常
mtx.unlock();
}
C++
作为一门面向对象语言,带有异常机制,一旦抛出异常,就会直接结束函数栈帧,一直跳转到cache
。以上代码中,如果抛异常了,那么mtx.unlock()
根本就不会执行,导致锁没法释放。
因此C++
引入了RAII
机制来管理锁,利用对象的生命周期来实现加锁和解锁,原理如下:
cpp
template <typename mutex_type>
class LockGuard
{
public:
LockGuard(mutex_type& lock)
: _lk(lock)
{
_lk.lock();
}
~LockGuard()
{
_lk.unlock();
}
private:
mutex_type& _lk;
};
void test(int n, std::mutex& mtx)
{
LockGuard<std::mutex> guard(mtx);
// 抛异常
}
LockGuard
这个类,在构造时接受一个锁,随后加锁,在析构时自动解锁。那么加锁的时间就与对象的生命周期绑定了。而就算经过异常退出,对象也会正常析构,从而加锁。
标准库std::lock_guard
就是这个原理,其接收一个锁类型的模板参数,在构造中调用lock
,析构中调用unlock
,完成对锁的自动管理。
cpp
void test(int n, std::mutex& mtx)
{
std::lock_guard<std::mutex> guard(mtx);
// 抛异常
}
使用了lock_guard
后,就不用再自己手动解锁了。
unique_lock
lock_guard
的可操作性很低,只有构造和析构两个函数,也就是只有自动释放锁的能力。而unique_lock
功能更加丰富,而且可以自由操作锁。
unique_lock
在构造时,可以传入一把锁,在构造的同时会对该锁进行加锁。在unique_lock
析构时,判断当前的锁有没有加锁,如果加锁了就先释放锁,后销毁对象。
而在构造与析构之间,也就是整个unique_lock
的生命周期,可以自由的加锁解锁:
lock
:加锁unlock
:解锁try_lock
:如果没上锁就加锁,上锁了就返回try_lock_until
:如果到指定时间还没申请到锁就返回false
,申请到锁返回true
try_lock_for
:如果一段时间内没申请到锁就返回false
,申请到锁返回true
提供了以上五个接口,也就是说可以作用于前面的任何一款锁。另外的unique_lcok
还允许赋值operator=
,调用赋值时,如果当前锁没有持有锁,那么直接拷贝。如果当前锁持有锁,那么把锁的所有权转移给新的unique_lcok
,自己不再持有锁。
atomic
在多线程情况下要加锁,就是因为很多操作不是原子性的。但是有一些简单的操作,比如num++
,每次都加锁解锁,性能必然会降低。因此C++
又提供了原子库<atomic>
,其实现了简单操作的原子化,一些简单的++、--
等都实现了原子化,可以不加锁也没有线程安全,需要头文件<atomic>
。
atomic
支持的类型如下:
- 可以通过简单的拷贝完成复制,而不需要调用构造函数与拷贝构造等
- 类型的大小不超过
std::atomic
的内部实现所支持的最大大小(通常是与机器字大小相同)
最常见的满足以上要求的类型就是内置类型,比如char
,各种整型,浮点型,以及指针。
使用起来也很简单:
cpp
atomic<类型> 变量名;
这样即可定义一个原子的类型。
示例:
cpp
std::atomic<int> num = 0;
void test(int n)
{
for (int i = 0; i < n; i++)
{
num++;
}
}
int main()
{
std::thread t1(test, 2000);
std::thread t2(test, 2000);
t1.join();
t2.join();
std::cout << "num = " << num << std::endl;
return 0;
}
以上是一个多线程代码,但是我们并没有加锁,却是线程安全的,因为num
是一个atomic<int>
类型的变量,num++
是一个原子操作。
atomic
类的成员函数如下:
首先就是实现了operator++
和operator--
,自增自减的操作是原子的。
再比如说fetch_add
,用于实现对一个原子类型增加指定值,该过程也是原子的。
cpp
std::atomic<int> num = 3;
num.fetch_add(5);
以上代码完成了3 + 5
的计算,且过程是原子性的,其余操作也是类似的:
fetch_add
:原子性,增加指定的值fetch_sub
:原子性,减少指定的值fetch_and
:原子性,与指定值按位与fetch_or
:原子性,与指定值按位或fetch_xor
:原子性,与指定值按位异或
还有一些其它接口:
store
用于设定原子类型为指定值:
cpp
std::atomic<int> num = 3;
num.store(100);
num.store(100)
相当于num = 100
,但是过程是原子性的。
load
用于获取原子类型当前的值,也是原子的。
operator T
是隐式类型转换,也就是从atomic<T>
转化为T
类型,此时就可以把原子类型当作一般类型来使用了,不过要注意的是,隐式转换后就是一般类型,不再具有原子性了!
CAS
C++
之所以可以实现变量的原子操作,是基于CAS
的原子操作,这是一个硬件级别的操作,其涉及三个操作数:
内存位置
预期值
更新值
操作流程为:读取内存位置的当前值
,判断是否与预期值
相等,如果相等,将其变为更新值
,如果不相等,返回当前值
。
比如在gcc
编译器中,内置了函数__sync_bool_compare_and_swap
,其用于进行CAS
操作:
cpp
bool __sync_bool_compare_and_swap(type* ptr, type oldval, type newval);
ptr
:内存位置,指向要操作的变量oldval
:预期值,即预计该变量原先的值newval
:更新值,希望把这个变量设置的值
返回值:如果修改成功返回true
,修改失败返回false
。
比如通过CAS
实现一个原子的自增:
cpp
while(__sync_bool_compare_and_swap(&x, x, x + 1) == false);
这样短短一行代码就可以实现原子自增,首先读取&x
,获取x
的地址,随后传入变量x
的当前值,预期值传入x + 1
,表示自增。
比如说在&x
后读取到了x
的当前值为5
,的此时另一个线程打断了操作,修改x = 10
。进入函数__sync_bool_compare_and_swap
后,发现预期值 = 5
,而当前x = 10
,说明被其他线程修改了,直接返回false
表示修改失败,进入下一轮while
循环。
也就是说基于CAS
实现的原子性,不是真的原子性,而是检测到在修改变量的过程中,有其它人来修改了变量,就终止操作防止线程安全错误。
这个函数在C/C++
里面是没法直接使用的,而是内置在编译器中,这是因为这个函数绕过了操作系统,直接与处理器的指令交互,因为CAS
操作要非常迅速,否则就会出现相互打断的问题。直接通过编译器与处理器的原子指令交互,比通过操作系统内核要快得多。
condition_variable
谈到锁,自然也要谈条件变量,这是线程同步的重要手段,C++
将条件变量放在头文件<condition_variable>
中。
condition_variable
condition_variable
只有一个无参的构造函数,且删除了拷贝构造,不允许拷贝。
- 等待:
wait
:进入条件变量的等待队列wait_for
:进入条件变量的等待队列,一定时间后如果没有被唤醒,则不再等待返回false
wait_until
:进入条件变量的等待队列,到指定时间后如果没有被唤醒,则不再等待返回false
第一个wait
需要传入一把锁unique_lock<mutex>
,此处要求必须使用unique_lock<mutex>
。而后续两个与时间相关的等待,分别要传入时间段duration
和时间点time_point
。至于为什么要传入一把锁,这属于并发编程的知识,就不在博客中讲解了。
- 唤醒:
notify_one
:唤醒等待队列的第一个线程notify_all
:唤醒等待队列的所有线程
示例:让两个线程从0 - 100
,轮流输出奇数偶数。
cpp
int n = 0;
bool flag = true;
std::mutex mtx;
std::condition_variable cv; // 条件变量
void func(bool run) // run用于标识是否轮到当前线程输出
{
while (n < 100)
{
std::unique_lock<std::mutex> lock(mtx);
while (flag != run) // 使用while代替if,防止伪唤醒
cv.wait(lock); // 没轮到当前线程,进入条件变量等待
std::cout << n << std::endl;
n++;
flag = !flag;
cv.notify_one();
}
}
int main()
{
std::thread t1(func, true); // falg == true 输出偶数
std::thread t2(func, false); // falg == false 输出奇数
t1.join();
t2.join();
return 0;
}
这就是一个简单的,两个线程通过条件变量相互制约的案例,展示了condition_variable
的基础用法。
再回到wait
函数,wait
的声明如下:
cpp
void wait (unique_lock<mutex>& lck);
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);
其有两个重载,第一个只有一个参数,也就是我刚刚提到的只要传入一个unique_lock<mutex>
。第二个重载允许传入第二个参数pred
,这是一个可调用对象,用于作为条件变量的判断值。
wait
的第二个参数要求是一个可调用对象,返回值类型伪bool
,作用如下:
- 返回
true
:表示条件成立,wait
直接返回,不进入等待队列 - 返回
false
:表示条件不成立,wait
阻塞,进入等待队列直到被唤醒
在刚刚的案例中,以下语句负责控制条件变量:
cpp
while (flag != run)
cv.wait(lock);
实际上在condition_variable
中,无需这样写判断语句,而是可以通过可调用对象传入条件变量内部:
cpp
void func(bool run)
{
auto cond_func = [&](){
return flag == run;
};
while (n < 100)
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, cond_func);
std::cout << n << std::endl;
n++;
flag = !flag;
cv.notify_one();
}
}
此处给wait
函数第二个参数传入了一个lambda
表达式cond_func
,其返回一个bool
值flag == run
,当这个值为true
说明轮到当前线程执行,也就是条件成立,当前线程不会进入等待队列。
另外的,在等待队列的线程被唤醒后,也会触发一次该函数的条件判断,防止伪唤醒。