多线程编程-杂

Last Updated on 2020年7月20日

重要的两个类型BlockingQueueCountDownLatch

  • 在我目前有限的实践中,这两个类型及其简单变种解决了至少90%的问题.
  • 这两个类的实现代码都非常简单.
  • BlockingQueue主要用于在线程之间传递数据.
  • CountDownLatch主要用于多个线程的简单同步控制.例如,Thread Fence (类似于CUDA的__syncthreads) 就可以通过CountDownLatch 实现.
// 这只是个最简单的例子,你可以依据自己的场景来修改其API
template <class ElementT>
class BlockingQueue
{
    void Push(const ElementT& val)
    {
        std::unique_lock<std::mutex> q_lock(q_mutex_);
        q_.push(val);
        cond_.notify_one();
    }

    ElementT Pop()
    {
        std::unique_lock<std::mutex> q_lock(q_mutex_);
        while (q_.empty())
        {
            cond_.wait(q_lock);
        }
        ElementT ret = q_.front();
        q_.pop();
        return ret;
    }

private:
    std::queue<ElementT> q_;
    std::mutex q_mutex_;
    std::condition_variable cond_;
};
class CountDownLatch
{
public:
    CountDownLatch(int n = 0): n_(n)
    {
    }

    void ResetCountAs(int new_count)
    {
        std::unique_lock<std::mutex> n_lock(n_mutex_);
        n_ = new_count;
    }

    void CountDown()
    {
        std::unique_lock<std::mutex> n_lock(n_mutex_);
        --n_;
        if (n_ == 0)
        {
            cond_.notify_all();
        }
    }

    void CountUp()
    {
        std::unique_lock<std::mutex> n_lock(n_mutex_);
        ++n_;
    }

    void Wait()
    {
        std::unique_lock<std::mutex> n_lock(n_mutex_);
        while (n_ != 0)
        {
            cond_.wait(n_lock);
        }
    }

private:
    int n_ = 0;
    std::mutex n_mutex_;
    std::condition_variable cond_;
};

经验谈

  • 一般而言,线程安全的class意思是: 使用class时,可以保证在不需要额外线程工具的前提下使用所有功能. 在这个标准下,STL中的绝大部分工具都是非线程安全的.
  • 如果你的代码可能有其他人引用,那么你就必须假定他是个傻子.因此,所有的线程安全都必须要由自己保证,而非用户代码.
  • 多线程环境中shared_ptr,weak_ptr,unique_ptr十分有用.如果可能,应当优先使用unique_ptr
  • 只使用mutex和condition进行开发.
    • 使用的工具越高级,将来引入的bug就可能越复杂. 而简单的工具往往仅需要比高级工具多走一步而已.
  • 尽可能使用高层次的设施,避免使用汇编/内核级的代码.
    • 例如,应当使用标准库的atomic,而不应当自己写lock-free的代码.
    • 原因很简单, 别人通常写的比你好.
  • std::mutexlock/unlock总应该通过std::unique_lock/std::lock_guard这样的工具实现.
  • 为了避免不必要的麻烦: 尽可能不要在多线程环境中使用fork和signal
  • 库最好不要自动开启新线程,把线程开启/关闭的控制权全部交给库用户.
  • 为了避免麻烦,只用return来结束线程.

读写锁的替代: shared_ptr + copy on write

  • 这个实现的主要特点是:使用shared_ptr是加快临界区的操作,降低读取时锁竞争的成本.
shared_ptr<Data> d;
mutex write_mutex;
mutex read_mutex;
void read()
{
    shared_ptr<Data> local;
    {
        lock_guard read_guard(read_mutex);
        local=d;
    }
    process(local)
}

void write(){

    lock_guard write_guard(write_mutex);// 只有一个线程能执行写入.

    shared_ptr<Data> new_data= get_new_data(); // 准备好新的数据.

    lock_guard read_guard(read_mutex);// 更新数据指针
    d=new_d;
}