c++ - 如何正确退出可能正在等待 std::condition_variable 的 std::thread?

标签 c++ multithreading boost c++11 circular-buffer

我有一个类使用互斥锁和两个条件变量实现线程化生产者/消费者系统以进行同步。当有元素要使用时,生产者向消费者线程发出信号,而消费者在消费完元素时向生产者线程发出信号。线程继续生产和消费,直到析构函数通过设置 bool 变量请求它们退出。因为任何一个线程都可能在等待条件变量,所以我必须对 quit 变量进行第二次检查,这感觉不对而且很乱......

我已将问题简化为以下(使用 g++4.7 在 GNU/Linux 上工作)示例:

// C++11and Boost required.
#include <cstdlib> // std::rand()
#include <cassert>

#include <boost/circular_buffer.hpp>

#include <atomic>
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

// Creates a single producer and single consumer thread.
class prosumer
{
    public:
        // Create the circular buffer and start the producer and consumer thread.
        prosumer()
            : quit_{ false }
            , buffer_{ circular_buffer_capacity }
            , producer_{ &prosumer::producer_func, this }
            , consumer_{ &prosumer::consumer_func, this }
        {}

        // Set the quit flag and wait for the threads to exit.
        ~prosumer()
        {
            quit_ = true;
            producer_.join();
            consumer_.join();
        }

    private:
        // Thread entry point for the producer.
        void producer_func()
        {
            // Value to add to the ringbuffer to simulate data.
            int counter = 0;

            while ( quit_ == false )
            {
                // Simulate the production of some data.
                std::vector< int > produced_items;
                const auto items_to_produce = std::rand() % circular_buffer_capacity;
                for ( int i = 0; i < items_to_produce; ++i )
                {
                    produced_items.push_back( ++counter );
                }

                // Get a lock on the circular buffer.
                std::unique_lock< std::mutex > lock( buffer_lock_ );

                // Wait for the buffer to be emptied or the quit flag to be set.
                buffer_is_empty_.wait( lock, [this]()
                        {
                            return buffer_.empty() == true || quit_ != false;
                        } );

                // Check if the thread was requested to quit.
                if ( quit_ != false )
                {
                    // Don't let the consumer deadlock.
                    buffer_has_data_.notify_one();
                    break;
                }

                // The buffer is locked by this thread. Put the data into it.
                buffer_.insert( std::end( buffer_ ), std::begin( produced_items ), std::end( produced_items ) );

                // Notify the consumer that the buffer has some data in it.
                buffer_has_data_.notify_one();
            }
            std::cout << "producer thread quit\n";
        }


        // Thread entry for the consumer.
        void consumer_func()
        {
            int counter_check = 0;

            while ( quit_ == false )
            {
                std::unique_lock< std::mutex > lock( buffer_lock_ );

                // Wait for the buffer to have some data before trying to read from it.
                buffer_has_data_.wait( lock, [this]()
                        {
                            return buffer_.empty() == false || quit_ != false;
                        } );

                // Check if the thread was requested to quit.
                if ( quit_ != false )
                {
                    // Don't let the producer deadlock.
                    buffer_is_empty_.notify_one();
                    break;
                }

                // The buffer is locked by this thread. Simulate consuming the data.
                for ( auto i : buffer_ ) assert( i == ++counter_check );
                buffer_.clear();

                // Notify the producer thread that the buffer is empty.
                buffer_is_empty_.notify_one();
            }
            std::cout << "consumer thread quit\n";
        }

        // How many items the circular buffer can hold. 
        static const int circular_buffer_capacity = 64;

        // Flag set in the destructor to signal the threads to stop.
        std::atomic_bool quit_;

        // Circular buffer to hold items and a mutex for synchronization.
        std::mutex buffer_lock_;
        boost::circular_buffer< int > buffer_;

        // Condition variables for the threads to signal each other.
        std::condition_variable buffer_has_data_;
        std::condition_variable buffer_is_empty_;

        std::thread producer_;
        std::thread consumer_;
};


int main( int argc, char **argv )
{
    (void)argc; (void) argv;

    prosumer test;

    // Let the prosumer work for a little while.
    std::this_thread::sleep_for( std::chrono::seconds( 3 ) );

    return EXIT_SUCCESS;
}

如果您查看 producer_func 和 consumer_func 线程函数,您会发现它们会循环直到 quit 变量被 prosumer 析构函数设置,但它们也会在锁定循环缓冲区后再次检查 quit 变量。如果设置了 quit 变量,它们会互相发送信号以防止死锁。

我的另一个想法是在析构函数的条件变量上调用 notify_one(),这会是更好的解决方案吗?

有更好的方法吗?

更新 1:我忘了说,在这种情况下,当线程被请求退出时,消费者不需要消耗循环缓冲区中的任何剩余数据,如果生产者也多生产一点也没关系。只要他们都退出并且不陷入僵局,一切都会好起来的。

最佳答案

在我看来,在调用 join 之前在析构函数中的两个条件变量上调用 notify_one(或者更确切地说是 notify_all,如果您要将缓冲区扩展到多个生产者/消费者)将是首选解决方案,原因如下:

首先,这符合条件变量的典型使用方式:通过设置 quit_,您可以更改生产者/消费者线程感兴趣并等待的状态,因此您应该通知他们状态更改。

此外,notify_one 不应该是一个非常昂贵的操作。

此外,在更实际的应用中,可能会出现在两个元素的生成之间存在延迟的情况;在这种情况下,您可能不想阻塞析构函数,直到消费者注意到它必须在下一个元素入队后立即取消;在示例代码中,据我所知,这并没有发生。

关于c++ - 如何正确退出可能正在等待 std::condition_variable 的 std::thread?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14151328/

相关文章:

c++ - 无法在 std::thread 中传递一维数组

c++ - odeint 中的推力和刚性 ODE 求解器

c++ - unordered_map emplace 编译错误

python - 在Python中并行读取多个文件

c++引用同级 namespace

c# - 如何暂停和继续 TPL 任务?

c++ - boost::hana 如何检查类型是否为 std::optional?

c++ - 共享内存中的 pcl::PointCloud

C++ pragma omp critical(名称)

c++ - 为什么将对象指针传递给删除方法的方法与直接删除对象不同?