互斥和同步
互斥和同步都是指某一个资源在某个时刻只能被一个访问者进行访问。
- 互斥不限制访问者对资源的访问顺序。
- 同步则是使用一定的机制来实现访问者对资源的有序访问。
轮询
假如你正在一辆夜里运行的火车上,很累,你的一种选择是不睡觉,一直醒着看到没到站。类似的,在并发程序中,当一个线程等待另一线程完成任务时也可以做出这样的选择。
具体地,结合信号量机制来轮询。由互斥量保护全局变量 flag,其初始化为 false,如果另一线程结束任务则置为 true。让一个线程不断地去查看这个 flag 标志是否为 true 来判断任务结束没有。
假设 man 线程先启动,那么观察到 flag 是 false,于是解锁休眠。这个时候 train 线程就有机会获取锁,然后开车并等待设置 flag。
注意,在 man 线程中由于只包含了对 flag 的读操作,其实是可以不加锁的。但为了避免其它线程对 flag 错误的写操作,于是加锁。
使用 std::this_thread::sleep_for
让线程休眠时,这个时间是不好确定的,太短就浪费CPU,太长并发性就不好。
所以我们考虑另一种方式,你睡去吧,到站了让乘务员叫你。
条件变量
条件变量很好地解决了上述例子中的问题,直接让等待的线程休眠,当发生了特定的条件时,唤醒等待线程。
使用条件变量,需要包含头文件 <condition_variable>
这里,我们定义了全局的条件变量 std::condition_variable。
当调用 wait 方法时,传递一个锁进去,wait方法会让该线程休眠,直到另一个线程调用 notify_one 才会将其唤醒。
于是就有个问题,假如 man 线程先拿到锁,然后在 wait 处等待,那么 train 线程怎么拿到锁然后开车呢?
- 这是因为一个线程是不会在持有锁的情况下休眠的,于是 wait 方法在将线程加入等待队列让出CPU时,会将传入的与 lk 关联的互斥量 mutex 解锁。于是 train 线程就有机会拿到锁了。
- 值得一提的是,在调用 wait 方法的线程被唤醒以后,会重新持有锁,然后去检查条件是否满足。如果满足,那么 wait 方法返回并继续持有锁;如果不满足,那么线程对其解锁并继续休眠。所以,与条件变量一起使用时,推荐使用更灵活的 unique_lock 而不是 lock_guard
- wait()的第二个参数可以传入一个函数表示检查条件,这里使用lambda函数最为简单,如果这个函数返回的是true,wait()函数不会阻塞会直接返回,如果这个函数返回的是false,wait()函数就会阻塞着等待唤醒,如果被伪唤醒,会继续判断函数返回值
这样,就避免了 man 线程很多次没有意义的轮询,大大节约了系统资源。
notify_all
当车上有很多人,我们想把他们都叫醒时,使用 notify_all
更方便管理的写法:
#include <iostream>
#include <vector>
#include <thread>
typedef void (*func)();
int atom;
void f1() { atom=1;printf("atom=%d\n",atom); }
void f2() { atom=2;printf("atom=%d\n",atom); }
void f3() { atom=3;printf("atom=%d\n",atom); }
int main()
{
std::vector<func> func_vec = { &f1, &f2, &f3 };
std::vector<std::thread> thread_vec;
for (func& f : func_vec)
thread_vec.emplace_back(std::thread(f));
for (std::thread& t : thread_vec)
t.join();
return 0;
}