【问题标题】:How to test my blocking queue actually blocks如何测试我的阻塞队列实际上阻塞
【发布时间】:2018-05-01 10:24:01
【问题描述】:

我有一个阻塞队列(我很难改变它的实现),我想测试它是否真的阻塞了。特别是,如果队列为空,pop 方法必须阻塞,并在执行 push 时立即解除阻塞。请参阅以下伪 C++11 代码进行测试:

BlockingQueue queue; // empty queue

thread pushThread([] 
{ 
    sleep(large_delay);
    queue.push(); 
});

queue.pop();

显然并不完美,因为有可能会发生整个线程pushThread在调用pop之前就已经执行完毕,即使延迟很大,而且延迟越大我要等待的时间越多测试结束。

如何正确确保在调用push 之前执行pop 并且在push 返回之前一直阻塞?

【问题讨论】:

  • 通常你不能在标准c++中,因为对于其他线程来说,阻塞线程看起来与调度程序休眠的线程相同。实际上,等待 50ms 左右就足够了。

标签: c++ multithreading unit-testing c++11 blocking


【解决方案1】:

如果不向 BlockingQueue 添加一些额外的状态和接口,我认为这是不可能的。

证明是这样的。您想等到pop 上的读取线程被阻塞。但是没有办法区分它和即将执行pop的线程。无论您在调用 pop 之前或之后添加什么内容,这都是正确的。

如果你真的想以 100% 的可靠性解决这个问题,你需要在队列中添加一些状态,由队列的互斥体保护,这意味着“有人在等待”。然后,pop 调用必须在它以原子方式释放互斥锁并在内部条件变量上进入休眠状态之前更新该状态。 push 线程可以获取互斥体并等待直到“有人在等待”。为避免此处出现繁忙循环,您将需要再次使用条件变量。

所有这些机制几乎与队列本身一样复杂,所以也许您也想测试它...这种多线程代码是“代码覆盖率”等概念的所在——甚至可以说是单元测试自己——分解一下。有太多可能的操作交错。

实际上,我可能会采用你原来的睡眠方式。

【讨论】:

    【解决方案2】:
    template<class T>
    struct async_queue {
      T pop() {
        auto l = lock();
        ++wait_count;
        cv.wait( l, [&]{ return !data.empty(); } );
        --wait_count;
        auto r = std::move(data.front());
        data.pop_front();
        return r;
      }
      void push(T in) {
        {
          auto l = lock();
          data.push_back( std::move(in) );
        }
        cv.notify_one();
      }
      void push_many(std::initializer_list<T> in) {
        {
          auto l = lock();
          for (auto&& x: in) 
            data.push_back( x );
        }
        cv.notify_all();
      }
      std::size_t readers_waiting() {
        return wait_count;
      }
      std::size_t data_waiting() const {
        auto l = lock();
        return data.size();
      }
    private:
      std::queue<T> data;
      std::condition_variable cv;
      mutable std::mutex m;
      std::atomic<std::size_t> wait_count{0};
      auto lock() const { return std::unique_lock<std::mutex>(m); }
    
    };
    

    或类似的。

    在推送线程中,忙于等待readers_waiting,直到它通过1。

    此时您拥有锁并且在解锁之前位于cv.wait 内。做一个push

    理论上,一个无限慢的读取器线程可能已经进入cv.wait,并且在您调用push 时仍在评估第一个 lambda,但是无限慢的读取器线程与阻塞的线程没有什么不同...

    不过,这确实会处理缓慢的线程启动等问题。

    readers_waitingdata_waiting 用于除调试以外的任何事情通常会产生代码异味。

    【讨论】:

    • OT:我更喜欢对 unique_locklock_guard 使用大括号初始化,来自关于“最令人头疼的解析”的个人(糟糕)经验,尽管这与给定代码无关。
    【解决方案3】:

    您可以使用std::condition_variable 来完成此操作。 cppreference.com 的帮助页面实际上显示了一个非常好的 cosumer-producer 示例,这应该正是您正在寻找的:http://en.cppreference.com/w/cpp/thread/condition_variable

    编辑:实际上德语版的 cppreference.com 有一个更好的例子 :-) http://de.cppreference.com/w/cpp/thread/condition_variable

    【讨论】:

    • 你会在哪里等待,你会在哪里发信号?
    • 启动两个线程,一个用于pop(),一个用于push。在pop() 之后等待并在push() 之后发出信号。确保在pop() 线程之后启动push() 线程。
    • @SamerTufail 我认为您只是增加了复杂性而没有真正解决问题。您如何确保在您的解决方案中 pop 之后调用 push?
    • @Nick,您在 pop 之后启动推送线程,并且可能在 pop 之后向推送线程发出信号,所以有两个条件变量。
    • 可能吗?如果主线程在那里被阻塞,你如何在 pop 之后启动线程?等待队列不为空?
    猜你喜欢
    • 2014-10-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多