现在有一道笔试题是下面这样子的。
有两个线程,一个线程循环输出A,另一个线程循环输出B,如何让这两个线程在控制台稳定输出ABABAB…。
不用思考太多,我们肯定会定义一个标志变量isTurnA
,isTurnA
为true
输出A,同理输出B,这是一种最简单的有限状态机,只要按照这个状态机进行,那么肯定能答应出答案。isTurnA
是共享数据,因此用原子变量或者互斥锁来保护,这里用互斥锁std::mutex
,代码如下:
class TypeAB
{
public:
TypeAB() : _isTurnA(true) {}
~TypeAB()
{
_threadA.join();
_threadB.join();
}
void PrintAB()
{
_threadA = std::thread(&TypeAB::TypeA, this);
std::this_thread::sleep_for(std::chrono::seconds(2));
_threadB = std::thread(&TypeAB::TypeB, this);
}
private:
std::mutex _mutex;
bool _isTurnA;
std::thread _threadA;
std::thread _threadB;
void TypeB()
{
for (int cnt = 0; cnt < 10; std::this_thread::sleep_for(std::chrono::seconds(3)))
{
std::unique_lock<std::mutex> lock(_mutex);
for (; _isTurnA; )
{
lock.unlock();
lock.lock();
}
std::cout << std::this_thread::get_id() << " - B : " << cnt++ << std::endl;
_isTurnA = true;
}
}
void TypeA()
{
for (int cnt = 0; cnt < 10; std::this_thread::sleep_for(std::chrono::seconds(1)))
{
std::unique_lock<std::mutex> lock(_mutex);
for (; !_isTurnA; )
{
lock.unlock();
lock.lock();
}
std::cout << std::this_thread::get_id() << " - A : " << cnt++ << std::endl;
_isTurnA = false;
}
}
};
这个代码是可以正常运行的,但是看一下下面这个循环:
for (; !_isTurnA; )
{
lock.unlock();
lock.lock();
}
不断解锁加锁,以便让另一条线程有机会得到锁,这样子做非常耗费效率。那么我们就来一个阻塞操作:
for (; !_isTurnA; )
{
lock.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
lock.lock();
}
这样子就会带来一个问题,有可能刚阻塞完并获取了锁,这个时候,另一个线程刚好设置isTurnA
,这种情况会造成当前线程多阻塞一次。因此,这个阻塞的时间太难平衡了,阻塞时间太短,那就和自旋没有差别;阻塞时间太长,就会导致线程处理问题不及时。
这个就回到了C++中的事件Event(基于条件变量的封装)中的那个坐火车的例子,条件变量就是这么来的,它的原理如下:
for (; !_isTurnA; )
{
lock.unlock();
// 线程阻塞于此,等待一个信号,有这个信号就不阻塞了
lock.lock();
}
具体可以看C++中的事件Event(基于条件变量的封装)中关于条件变量的介绍。下面给出这道多线程题的标答:
#include
class TypeAB
{
public:
~TypeAB()
{
_threadA.join();
_threadB.join();
}
void PrintAB()
{
_threadA = std::thread(&TypeAB::TypeA, this);
std::this_thread::sleep_for(std::chrono::seconds(2));
_threadB = std::thread(&TypeAB::TypeB, this);
}
private:
std::mutex _mutex;
std::condition_variable _condi;
bool _isTurnA = true;
std::thread _threadA;
std::thread _threadB;
void TypeB()
{
for (int cnt = 0; cnt < 10; std::this_thread::sleep_for(std::chrono::seconds(3)))
{
std::unique_lock<std::mutex> lock(_mutex);
_condi.wait(lock, [this] () -> bool { return !_isTurnA; });
std::cout << std::this_thread::get_id() << " - B : " << cnt++ << std::endl;
_isTurnA = true;
lock.unlock();
_condi.notify_one();
}
}
void TypeA()
{
for (int cnt = 0; cnt < 10; std::this_thread::sleep_for(std::chrono::seconds(1)))
{
std::unique_lock<std::mutex> lock(_mutex);
_condi.wait(lock, [this] () -> bool { return _isTurnA; });
std::cout << std::this_thread::get_id() << " - A : " << cnt++ << std::endl;
_isTurnA = false;
lock.unlock();
_condi.notify_one();
}
}
};
int main()
{
TypeAB().PrintAB();
return 0;
}
现在来看看std::condition_variable
的部分源码:
// VC14的mutex头文件
class condition_variable
{ // class for waiting for conditions
public:
typedef _Cnd_t native_handle_type;
condition_variable()
{ // construct
_Cnd_init_in_situ(_Mycnd());
}
~condition_variable() _NOEXCEPT
{ // destroy
_Cnd_destroy_in_situ(_Mycnd());
}
condition_variable(const condition_variable &) = delete;
condition_variable &operator=(const condition_variable &) = delete;
void notify_one() _NOEXCEPT
{ // wake up one waiter
_Cnd_signalX(_Mycnd());
}
void notify_all() _NOEXCEPT
{ // wake up all waiters
_Cnd_broadcastX(_Mycnd());
}
void wait(unique_lock<mutex> &_Lck)
{ // wait for signal
_Cnd_waitX(_Mycnd(), _Lck.mutex()->_Mymtx());
}
template <class _Predicate>
void wait(unique_lock<mutex> &_Lck, _Predicate _Pred)
{ // wait for signal and test predicate
while (!_Pred())
wait(_Lck);
}
}
这段代码验证的下面的几个问题:
wait
的写法推荐第二种,可以预防虚假唤醒std::unique_lock
,原因有二,std::unique_lock
有加锁解锁成员函数,完全契合条件变量的工作原理;RAII模式,可以保证异常安全std::condition_variable
构成std::future
埋下伏笔