[cpp] cpp11--condition_variable(条件变量)
目录
1. cpp11-> std::condition_variable
2. this_thread::sleep_for
3. 经典问题: 两个线程相互打印偶数和奇数
1. cpp11-> std::condition_variable
基本和 posix 提供的条件变量是一样的. 里面有两个核心操作:
- wait(等待)
- notify(唤醒)
注意: 条件变量本身不是线程安全的, 所以它本身也需要互斥锁的保护.
我们下面介绍一下 c++11 里面条件变量两个接口:
- wait: 阻塞的意思 -> 解锁 + 阻塞, 直到
notify
.
The execution of the current thread (which shall have locked lck's mutex) is blocked until notified.
At the moment of blocking the thread, the function automatically calls lck.unlock()
, allowing other locked threads to continue.
Once notified (explicitly, by some other thread), the function unblocks and calls lck.lock()
, leaving lck in the same state as when the function was called. Then the function returns (notice that this last mutex locking may block again the thread before returning).
- notify: 是唤醒的意思
Notify one
Unblocks one of the threads currently waiting for this condition.
If no threads are waiting, the function does nothing.
If more than one, it is unspecified which of the threads is selected.
Notify all
Unblocks all threads currently waiting for this condition.
If no threads are waiting, the function does nothing.
好的, 我们下面继续看看文档上怎么举得例子:
// condition_variable example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) cv.wait(lck);
// ...
std::cout << "thread " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all();
}
int main()
{
std::thread threads[10];
// spawn 10 threads:
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10 threads ready to race...\n";
go(); // go!
for (auto& th : threads) th.join();
return 0;
}
啥意思呢上面代码?
首先, 它弄了一个数组, 每个元素都是一个空壳的 thread, 只有空间没有实际的值哦~
然后 for 循环, 依次对这十个空壳的 thread 发生移动构造, 是用临时变量 std::thread(print_id, i) 来进行移动构造, 每个线程会立刻去执行print_id
, 每个线程来到 print_id 之后, 会先加锁, 然后十个线程里抢到锁的执行 while , wait 一下, 归还锁, 然后剩下 9 个线程依次.... 理论上, 时间足够的话, 所有的线程都会到达 wait.
然后主线程同时继续向下运行, 把 ready = true
. 所有的线程继续竞争锁, 竞争到的出 while 然后打印自己的 id 值... 以此类推.
我们看, 当10个线程起来了之后, 对于每一个线程来说, 要么你抢不到锁, 然后就会阻塞在 12 行, 要么你比较幸运抢到了锁, 然后wait.
理论上, 只要时间比较长, 应该所有线程到最后都会跑到 13 行进行wait等待.
然后, 上面代码来到go()函数, 先弄了一个锁, 然后把所有的线程唤醒.
有没有可能在go()之前, 还有一些线程没有进入wait状态呢?
有可能. 也有可能有些线程不会进入wait状态, 直接掠过while(!ready)来到206行进行打印.
我们执行一下: ... 发现0号线程最容易第一个打印.
我们为了让他都进入wait()
阻塞, 我们用一下sleep
.
2. this_thread::sleep_for
我们下面介绍一个 sleep_for() 它可以让线程等待休眠一段时间再继续运行, 这样的话我们让主线程等几百毫秒, 等所有的线程到到达再进行竞争更加公平一些.
Sleep for time span
Blocks execution of the calling thread during the span of time specified by rel_time.
The execution of the current thread is stopped until at least rel_time has passed from now. Other threads continue their execution.
然后, 他的一些时间定义在 duration
类当中, 我们下面来看一下:
Duration
A duration object expresses a time span by means of a count and a period.
Internally, the object stores the count as an object of member type rep (an alias of the first template parameter, Rep), which can be retrieved by calling member function count.
This count is expresed in terms of periods. The length of a period is integrated in the type (on compile time) by its second template parameter (Period), which is a ratio type that expresses the number (or fraction) of seconds that elapse in each period.
我们再继续运行一下上面的程序, 不过是在主线程加上 sleep_for
, 发现更加随机一些了:
3. 经典问题: 两个线程相互打印偶数和奇数
这个地方有一个经典的问题:
两个线程交替打印, 一个打印奇数, 一个打印偶数. 所以, 他需要一个线程打印完了, 就通知另一个线程再打印偶数, 再来回交替的.
约定: 我们为了好写代码, 我们规定 th1 打印偶数, 从 0 开始打印, 0, 2, 4 ..., 然后 th2 打印奇数, 从 1 开始打印, 1, 3, 5 ... 整体的打印逻辑是 th1 先打印 0, th2 再打印 1, th1 再打印 2, th2 打印 3... 直到 99.
#include<thread>
#include<iostream>
#include<mutex>
#include<condition_variable>
using namespace std;
mutex mtx;
condition_variable c;
int flag = false;
void pinrtOddNumber(int n)
{
int i = 1;
while (i < n)
{
unique_lock<mutex> lg(mtx); // 加锁
while (!flag) // 是false的时候会被wait.
c.wait(lg);
cout << "th2(odd): " << i << endl; // 临界资源!!!
flag = false;
i += 2;
c.notify_one(); // 唤醒
}
}
void pinrtEvenNumber(int n)
{
int i = 0;
while (i < n)
{
unique_lock<mutex> lg(mtx); // 加锁
while (flag) // true的时候会被wait()
c.wait(lg);
cout << "th1(eve): " << i << endl; // 临界资源!!!
flag = true;
i += 2;
c.notify_one(); // 唤醒
}
}
int main()
{
thread t1(pinrtEvenNumber, 100);
thread t2(pinrtOddNumber, 100);
t1.join();
t2.join();
return 0;
}
我们看一下结果:
下面说一下细节:
while() wait()
是做什么的? 是为了防止奇数先打印 或者是 防止有一个连续打印的.c.notify_one();
这个的意义是啥? 首先一个在休眠吧? 还有一个因为自己把 flag 设置了, 所以等会儿自己也会休眠, 所以这个快休眠的在自己休眠之前先唤醒一下另一个线程.
不过还有一种用 lambda 写法, 感觉是一样的, 只是写法上不同而已.
#include <thread>
#include <mutex>
#include <condition_variable>
#include <iostream>
void two_thread_print() {
std::mutex mtx;
std::condition_variable c;
int n = 100;
bool flag = true;
std::thread t1([&]() {
int i = 0;
while (i < n) {
std::unique_lock<std::mutex> lock(mtx); // 加锁
c.wait(lock, [&]() -> bool { return flag; }); // 是否还锁?
std::cout << i << std::endl; // 打印
flag = false; // 设置falg.
i += 2; // 偶数
c.notify_one(); // 唤醒
}
});
std::thread t2([&]() {
int j = 1;
while (j < n) {
std::unique_lock<std::mutex> lock(mtx);
c.wait(lock, [&]() -> bool { return !flag; });
std::cout << j << std::endl;
j += 2; // 奇数
flag = true;
c.notify_one();
}
});
t1.join();
t2.join();
}
int main()
{
two_thread_print();
return 0;
}