C++:线程库
- thread
- thread
- this_thread
- chrono
- 引用拷贝问题
- mutex
- mutex
- timed_mutex
- recursive_mutex
- lock_guard
- unique_lock
- atomic
- atomic
- CAS
- condition_variable
- condition_variable
thread
操作线程需要头文件<thread>,头文件包含线程相关操作,内含两个内容:
thread类:操作线程的基本类this_thread命名空间域:用于操作当前线程
thread
thread是用于操作线程的类,其实现了对多平台线程的封装,以统一的面向对象方式完成线程的操作。
thread构造:
thread() noexcept;
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
thread (const thread&) = delete;
thread (thread&& x) noexcept;
从以上构造的声明可以得出以下要点:
- 可以不传参,直接构造一个空线程对象
- 禁止拷贝构造
- 支持移动构造
其中第二个声明是最重要的:
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
这是一个函数模板,参数如下:
fn:一个可调用对象,创建线程后线程执行该函数内容args:可变参数包,用于给fn传参,可以传任意数量的参数
示例:
#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线程类的赋值重载,同样禁止拷贝赋值,只允许移动赋值。
还记得我们可以创建一个空线程类吗,没有函数可以给一个空线程类绑定函数,只能通过移动赋值来完成初始化空线程类。
示例:
#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,其声明如下:
id get_id() const noexcept;
如果仔细观察返回值,你会发现其返回的不是一个整数,而是一个叫做id的类型。其是thread的内部类,由于不同操作系统对线程的标识符不同,比如Linux使用整数tid标识,而Windows使用线程句柄标识不同线程。所以无法统一线程的id,因此C++将不同操作系统的标识符封装为一个类thread::id。
thread::id重载了以下操作符:
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可以直接在哈希表内部使用。
template <class T> struct hash; // 通用模板声明
template<> struct hash<thread::id>; // 对 thread::id 的模板特化
另外的,thread::id还支持流输出:
template <class charT, class traits>
basic_ostream<chasrT, traits>& operator<< (basic_ostream<charT,traits>& os, thread::id id);
可以直接用cout这样的流输出对象来输出id:
std::thread th(test, 100, 500);
std::cout << th.get_id() << std::endl;
join
用于回收线程,调用该函数后,主线程进入阻塞,直到被join的线程结束,最后回收该线程。
示例:
#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。
#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会受到系统时钟影响,如果用户调整了系统时间,就有可能造成时间错误,而稳定时钟不受系统时钟影响。
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,如果想要表示一个时间端,直接传数字即可:
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完成睡眠:
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,同样的睡眠十秒:
auto dur = std::chrono::seconds(10); // 获取十秒时间段
std::this_thread::sleep_for(dur); // 睡眠到 10 秒后
引用拷贝问题
再看thread类的声明:
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
此处可以发现,其构造参数使用的是&&引用折叠,可以传左值/右值引用。
尝试一下:
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传引用
引用包装器:
引用包装器用于解决引用的拷贝问题,引用拷贝后依然是原先的变量。
引用包装器的原理如下:
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()。
对于刚才的线程问题,只需要在传递参数时用包装器包装一层即可:
std::thread t1(test, std::ref(a), std::ref(b));
结合lambda:
除了以上两种方式,也可以直接使用lambda的捕捉列表,以引用的形式捕捉变量:
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的使用非常简单。
示例:
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++不是原子性的。此时对其加锁:
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,申请到锁返回truetry_lock_for:如果一段时间内没申请到锁就返回false,申请到锁返回true
通过之前的经验,可以猜出try_lock_until要传入一个time_point时间点,而try_lock_for要传入一个时间段duration。
以try_lock_for为例:
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
递归锁用于解决函数的递归造成的死锁,比如这样:
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加锁,第二次递归时由于自己已经占有锁了,再次申请锁就会阻塞,导致死锁。
递归锁就是用于解决这样的自己与自己造成的死锁局面。
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
在使用锁的过程中,最忌讳的就是忘记解锁,这就会导致一个线程一直持有锁,其他线程无法访问到资源。但是难道每次用完锁后解锁,就可以保证锁被释放吗?
看到这个例子:
void test(int n, std::mutex& mtx)
{
mtx.lock();
// 抛异常
mtx.unlock();
}
C++作为一门面向对象语言,带有异常机制,一旦抛出异常,就会直接结束函数栈帧,一直跳转到cache。以上代码中,如果抛异常了,那么mtx.unlock()根本就不会执行,导致锁没法释放。
因此C++引入了RAII机制来管理锁,利用对象的生命周期来实现加锁和解锁,原理如下:
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,完成对锁的自动管理。
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,申请到锁返回truetry_lock_for:如果一段时间内没申请到锁就返回false,申请到锁返回true
提供了以上五个接口,也就是说可以作用于前面的任何一款锁。另外的unique_lcok还允许赋值operator=,调用赋值时,如果当前锁没有持有锁,那么直接拷贝。如果当前锁持有锁,那么把锁的所有权转移给新的unique_lcok,自己不再持有锁。
atomic
在多线程情况下要加锁,就是因为很多操作不是原子性的。但是有一些简单的操作,比如num++,每次都加锁解锁,性能必然会降低。因此C++又提供了原子库<atomic>,其实现了简单操作的原子化,一些简单的++、--等都实现了原子化,可以不加锁也没有线程安全,需要头文件<atomic>。
atomic
支持的类型如下:
- 可以通过简单的拷贝完成复制,而不需要调用构造函数与拷贝构造等
- 类型的大小不超过
std::atomic的内部实现所支持的最大大小(通常是与机器字大小相同)
最常见的满足以上要求的类型就是内置类型,比如char,各种整型,浮点型,以及指针。
使用起来也很简单:
atomic<类型> 变量名;
这样即可定义一个原子的类型。
示例:
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,用于实现对一个原子类型增加指定值,该过程也是原子的。
std::atomic<int> num = 3;
num.fetch_add(5);
以上代码完成了3 + 5的计算,且过程是原子性的,其余操作也是类似的:
fetch_add:原子性,增加指定的值fetch_sub:原子性,减少指定的值fetch_and:原子性,与指定值按位与fetch_or:原子性,与指定值按位或fetch_xor:原子性,与指定值按位异或
还有一些其它接口:

store用于设定原子类型为指定值:
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操作:
bool __sync_bool_compare_and_swap(type* ptr, type oldval, type newval);
ptr:内存位置,指向要操作的变量oldval:预期值,即预计该变量原先的值newval:更新值,希望把这个变量设置的值
返回值:如果修改成功返回true,修改失败返回false。
比如通过CAS实现一个原子的自增:
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:进入条件变量的等待队列,一定时间后如果没有被唤醒,则不再等待返回falsewait_until:进入条件变量的等待队列,到指定时间后如果没有被唤醒,则不再等待返回false
第一个wait需要传入一把锁unique_lock<mutex>,此处要求必须使用unique_lock<mutex>。而后续两个与时间相关的等待,分别要传入时间段duration和时间点time_point。至于为什么要传入一把锁,这属于并发编程的知识,就不在博客中讲解了。
- 唤醒:
notify_one:唤醒等待队列的第一个线程notify_all:唤醒等待队列的所有线程
示例:让两个线程从0 - 100,轮流输出奇数偶数。
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的声明如下:
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阻塞,进入等待队列直到被唤醒
在刚刚的案例中,以下语句负责控制条件变量:
while (flag != run)
cv.wait(lock);
实际上在condition_variable中,无需这样写判断语句,而是可以通过可调用对象传入条件变量内部:
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说明轮到当前线程执行,也就是条件成立,当前线程不会进入等待队列。
另外的,在等待队列的线程被唤醒后,也会触发一次该函数的条件判断,防止伪唤醒。



















