thread
操纵线程必要头文件<thread>,头文件包罗线程相干操纵,内含两个内容:
- thread类:操纵线程的根本类
- this_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,创建线程时传入的函数,都只允许传入一个参数,而此处可以通过参数包传入恣意数量的参数。
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循环内部,完成对所有空线程对象的初始化。
直接获取一个线程的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的线程竣事,末了接纳该线程。
示例:
- #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()就可以完成线程资源的接纳,还是比力方便的。
用于线程分离,被分离的线程将自己接纳自己,无需再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还没有来得及输出,就被强制退出了。
用于检测一个线程是否允许被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命名空间域,该域内部封装了各种时间的相干操纵。
- 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用于表现一个时间段,这个类的用法比力复杂,因此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表现一个时间点,之前时钟返回的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,申请到锁返回true
- try_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,申请到锁返回true
- try_lock_for:如果一段时间内没申请到锁就返回false,申请到锁返回true
提供了以上五个接口,也就是说可以作用于前面的任何一款锁。别的的unique_lcok还允许赋值operator=,调用赋值时,如果当前锁没有持有锁,那么直接拷贝。如果当前锁持有锁,那么把锁的所有权转移给新的unique_lcok,自己不再持有锁。
atomic
在多线程环境下要加锁,就是因为很多操纵不是原子性的。但是有一些简单的操纵,比如num++,每次都加锁解锁,性能必然会低落。因此C++又提供了原子库<atomic>,其实现了简单操纵的原子化,一些简单的++、--等都实现了原子化,可以不加锁也没有线程安全,必要头文件<atomic>。
atomic
支持的范例如下:
- 可以通过简单的拷贝完成复制,而不必要调用构造函数与拷贝构造等
- 范例的巨细不高出 std::atomic 的内部实现所支持的最大巨细(通常是与呆板字巨细相同)
最常见的满足以上要求的范例就是内置范例,比如char,各种整型,浮点型,以及指针。
利用起来也很简单:
如许即可定义一个原子的范例。
示例:
- 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:进入条件变量的等待队列,肯定时间后如果没有被唤醒,则不再等待返回false
- wait_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说明轮到当前线程执行,也就是条件建立,当前线程不会进入等待队列。
别的的,在等待队列的线程被唤醒后,也会触发一次该函数的条件判断,防止伪唤醒。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |