使用互斥锁和条件变量进行线程同步

时间:2018-05-07 14:17:18

标签: multithreading c++11 mutex condition-variable

我试图实现多线程作业,生产者和消费者,基本上我想要做的是,当消费者完成数据时,它会通知生产者,以便生产者提供新数据。 / p>

棘手的部分是,在我目前的impl中,制作人和消费者都互相通知并互相等待,我不知道如何正确地实现这部分。

例如,请参阅下面的代码,

mutex m;
condition_variable cv;

vector<int> Q;  // this is the queue the consumer will consume
vector<int> Q_buf;  // this is a buffer Q into which producer will fill new data directly

// consumer
void consume() {
  while (1) {
    if (Q.size() == 0) {  // when consumer finishes data
      unique_lock<mutex> lk(m);
      // how to notify producer to fill up the Q?
      ...
      cv.wait(lk);
    }

    // for-loop to process the elems in Q
    ...
  }
}

// producer
void produce() {
  while (1) {
    // for-loop to fill up Q_buf
    ...

    // once Q_buf is fully filled, wait until consumer asks to give it a full Q
    unique_lock<mutex> lk(m);
    cv.wait(lk);
    Q.swap(Q_buf);  // replace the empty Q with the full Q_buf
    cv.notify_one();
  }
}

我不确定使用mutexcondition_variable的上述代码是实现我的想法的正确方法, 请给我一些建议!

5 个答案:

答案 0 :(得分:9)

代码错误地假设vector<int>::size()vector<int>::swap()是原子的。他们不是。

此外,spurious wakeups必须由while循环(或其他cv::wait重载)处理。

修正:

mutex m;
condition_variable cv;
vector<int> Q;

// consumer
void consume() {
    while(1) {
        // Get the new elements.
        vector<int> new_elements;
        {
            unique_lock<mutex> lk(m);
            while(Q.empty())
                cv.wait(lk);
            new_elements.swap(Q);
        }
        // for-loop to process the elems in new_elements
    }
}

// producer
void produce() {
    while(1) {
        vector<int> new_elements;
        // for-loop to fill up new_elements

        // publish new_elements
        {
            unique_lock<mutex> lk(m);
            Q.insert(Q.end(), new_elements.begin(), new_elements.end());
            cv.notify_one();
        }
    }
}

答案 1 :(得分:4)

也许这接近你想要的东西。我使用了2个条件变量来相互通知生产者和消费者,并引入了表示现在转向的变量:

#include <ctime>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>

template<typename T>
class ReaderWriter {
    private:
        std::vector<std::thread> readers;
        std::vector<std::thread> writers;
        std::condition_variable readerCv, writerCv;
        std::queue<T> data;
        std::mutex readerMutex, writerMutex;
        size_t noReaders, noWriters;
        enum class Turn { WRITER_TURN, READER_TURN };
        Turn turn;
        void reader() {
            while (1) {
                {
                    std::unique_lock<std::mutex> lk(readerMutex);    
                    while (turn != Turn::READER_TURN) {
                        readerCv.wait(lk);
                    }
                    std::cout << "Thread : " << std::this_thread::get_id() << " consumed " << data.front() << std::endl;
                    data.pop();
                    if (data.empty()) {
                        turn = Turn::WRITER_TURN;
                        writerCv.notify_one();
                    }
                }
            }
        }

        void writer() {
            while (1) {
                {
                    std::unique_lock<std::mutex> lk(writerMutex);
                    while (turn != Turn::WRITER_TURN) {
                        writerCv.wait(lk);
                    }
                    srand(time(NULL));
                    int random_number = std::rand();
                    data.push(random_number);
                    std::cout << "Thread : " << std::this_thread::get_id() << " produced " << random_number << std::endl;
                    turn = Turn::READER_TURN;
                }
                readerCv.notify_one();
            }
        }

    public:
        ReaderWriter(size_t noReadersArg, size_t noWritersArg) : noReaders(noReadersArg), noWriters(noWritersArg), turn(ReaderWriter::Turn::WRITER_TURN) {
        }

        void run() {
            int noReadersArg = noReaders, noWritersArg = noWriters;
            while (noReadersArg--) {
                readers.emplace_back(&ReaderWriter::reader, this);
            }

            while (noWritersArg--) {
                writers.emplace_back(&ReaderWriter::writer, this);
            }
        }

        ~ReaderWriter() {
            for (auto& r : readers) {
                r.join();
            }
            for (auto& w : writers) {
                w.join();
            }
        }
};

int main() {
    ReaderWriter<int> rw(5, 5);
    rw.run();
}

答案 2 :(得分:3)

这是一段代码片段。由于工人踏板已经同步,因此排除了对两个缓冲器的要求。因此,使用简单队列来模拟场景:

cond = lambda l: l.startswith("Percentage of adjacent deep bass") or l.startswith("Loading")
lines = [line for line in text.strip().split("\n") if cond(line)]

>>> lines
["Loading /Volumes/My Passport for Mac/Jonas's iTunes/#4 falling.mp3",
 'Percentage of adjacent deep bass frames: 0.125822 ',
 "Loading /Volumes/My Passport for Mac/Jonas's iTunes/#OccupyHipHop Inst.mp3",
 'Percentage of adjacent deep bass frames: 0.300660 ']

答案 3 :(得分:3)

虽然我认为两个条件变量可能有用,一个名为buffer_empty,生产者线程将等待,另一个名为buffer_filled,消费者线程将等待,但不是一个完整的答案。互斥锁的数量,如何循环等等我无法评论,因为我自己不确定细节。

答案 4 :(得分:2)

  1. 只有在持有时才能访问共享变量 保护它的互斥锁
  2. condition_variable::wait应检查一个条件。
    1. 条件应该是由传递给condition_variable::wait的互斥锁保护的共享变量。
    2. 检查条件的方法是在while循环中将调用包装到wait或使用wait的2参数重载(这是 相当于while循环版本)
  3. 注意:如果您真正了解硬件正在做什么,则这些规则并非绝对必要。但是,当使用简单的数据结构时,这些问题会很快变得复杂,如果您遵循它们,将更容易证明您的算法正常工作。

    您的QQ_buf是共享变量。由于规则1,我更愿意将它们作为在使用它们的函数中声明的局部变量(分别为consume()produce())。将有一个受互斥锁保护的共享缓冲区。生产者将添加到其本地缓冲区。当该缓冲区已满时,它将获取互斥锁并将本地缓冲区推送到共享缓冲区。然后,它会在生成更多数据之前等待消费者接受此缓冲区。

    使用者等待此共享缓冲区“到达”,然后它获取互斥锁并用共享缓冲区替换其空本地缓冲区。然后它向生产者发出信号,告知缓冲区已被接受,因此它知道再次开始生产。

    从语义上讲,我没有理由使用swap而不是move,因为在每种情况下,其中一个容器都是空的。也许你想使用swap因为你对底层内存有所了解。你可以使用你想要的任何一个,它会很快并且工作方式相同(至少在算法上)。

    这个问题可以通过1个条件变量来完成,但如果使用2,可能会更容易思考。

    这就是我想出的。在Visual Studio 2017(15.6.7)和GCC 5.4.0上测试。我不需要记入任何东西(这是一件如此简单的作品),但从法律上讲,我不得不说我不提供任何保证。

    #include <thread>
    #include <vector>
    #include <mutex>
    #include <condition_variable>
    #include <chrono>
    
    
    std::vector<int> g_deliveryBuffer;
    bool g_quit = false;
    std::mutex g_mutex;  // protects g_deliveryBuffer and g_quit
    std::condition_variable g_producerDeliver;
    std::condition_variable g_consumerAccepted;
    
    
    // consumer
    void consume() 
    {
        // local buffer
        std::vector<int> consumerBuffer;
    
        while (true)
        {
            if (consumerBuffer.empty())
            {  
                std::unique_lock<std::mutex> lock(g_mutex);
                while (g_deliveryBuffer.empty() && !g_quit)  // if we beat the producer, wait for them to push to the deliverybuffer
                    g_producerDeliver.wait(lock);
                if (g_quit)
                    break;
                consumerBuffer = std::move(g_deliveryBuffer);  // get the buffer
            }
            g_consumerAccepted.notify_one();  // notify the producer that the buffer has been accepted
    
            // for-loop to process the elems in Q
            // ...
            consumerBuffer.clear();
            // ...
        }
    }
    
    
    // producer
    void produce() 
    {
        std::vector<int> producerBuffer;
        while (true) 
        {
            // for-loop to fill up Q_buf
            // ...
            producerBuffer = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
            // ...
    
            // once Q_buf is fully filled, wait until consumer asks to give it a full Q
            {   // scope is for lock
                std::unique_lock<std::mutex> lock(g_mutex);
                g_deliveryBuffer = std::move(producerBuffer);  // ok to push to deliverybuffer. it is guaranteed to be empty
                g_producerDeliver.notify_one();
                while (!g_deliveryBuffer.empty() && !g_quit)
                    g_consumerAccepted.wait(lock);  // wait for consumer to signal for more data
                if (g_quit)
                    break;
                // We will never reach this point if the buffer is not empty.
            }
        }
    }
    
    
    
    int main()
    {
        // spawn threads
        std::thread consumerThread(consume);
        std::thread producerThread(produce);
    
        // for for 5 seconds
        std::this_thread::sleep_for(std::chrono::seconds(5));
    
        // signal that it's time to quit
        {
            std::lock_guard<std::mutex> lock(g_mutex);
            g_quit = true;
        }
        // one of the threads may be sleeping
        g_consumerAccepted.notify_one();
        g_producerDeliver.notify_one();
    
        consumerThread.join();
        producerThread.join();
    
        return 0;
    }