从一道多线程题来看C++11中条件变量std::condition_variable的使用和原理

现在有一道笔试题是下面这样子的。

有两个线程,一个线程循环输出A,另一个线程循环输出B,如何让这两个线程在控制台稳定输出ABABAB…

不用思考太多,我们肯定会定义一个标志变量isTurnAisTurnAtrue输出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 <bits/stdc++.h>

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埋下伏笔
发布了299 篇原创文章 · 获赞 353 · 访问量 45万+

猜你喜欢

转载自blog.csdn.net/FlushHip/article/details/88311749