我正在学习C ++(14),目前我正在研究并发部分。
我已经编写了一个小producer/consumer
示例来学习和理解condition_variables
。
这个想法是让一个线程用数字填充一个向量,然后用另一个线程将它打印到控制台:
#include <iostream>
#include <thread>
#include <condition_variable>
#include <vector>
#include <atomic>
std::atomic<bool> done{false};
std::mutex ready_mutex;
std::condition_variable ready;
std::vector<int> numbers;
void produce() {
auto idx = 0;
while( true ) {
++idx;
std::lock_guard<std::mutex> lk(ready_mutex);
for(int i = 0; i < 5; ++i) {
numbers.push_back(i);
}
if( 5 == idx) {
done = true;
break;
}
ready.notify_one();
}
ready.notify_one();
}
void consume() {
while( true ) {
std::unique_lock<std::mutex> lk(ready_mutex);
ready.wait(lk, [](){ return !v.empty(); });
std::cout << "(" << numbers.size() << ")" << std::endl;
for(auto x: numbers) {
std::cout << x << ", ";
}
std::cout << std::endl;
numbers.clear();
std::cout << "(" << numbers.size() << ")" << std::endl;
std::cout.flush();
if(done) break;
}
}
int main() {
std::thread t(produce);
std::thread t2(consume);
t.join();
t2.join();
return 0;
}
问题是该程序没有向我显示预期的输出。 我期待的是:
(5) 0, 1, 2, 3, 4, (0)
(5) 0, 1, 2, 3, 4, (0)
(5) 0, 1, 2, 3, 4, (0)
(5) 0, 1, 2, 3, 4, (0)
(5) 0, 1, 2, 3, 4, (0)
但我得到的是:
(25)
0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4,
(0)
对于我所看到的,producer
线程一次性运行所有迭代,然后通知consumer
线程,但几个小时后我仍然不明白为什么{{1直到最后都没有唤醒producer
。
此外,我注意到如果不使用全局向量而将此问题作为对线程的引用传递,则此问题仍然存在:
consumer
操作系统: Debian 9
编译器: g ++ 6.3.0,clang ++ 3.8.1-24
编译标志: -Werror -Wextra -Wall -std = c ++ 14 -O0 -g3 -pthread
答案 0 :(得分:0)
这里的问题是当生产者释放锁时,你期望消费者接下来获得锁。但是,由于生产者中的while循环,生产者可能会在消费者可以之前重新获得锁定。如果生产者只“生产”一次然后屈服于消费者,那就不会有问题,但由于你想要乒乓效应,你需要确保生产者等待消费者等待消费者等待的方式轮到它了。
解决这个问题的一种方法是使用另一个布尔值。我将其命名为canProduce
,并以与设置消费者相同的方式设置生产者。我还将在主要内容中重复整个过程15次,以最大限度地减少侥幸误报的可能性,如果制作人仍然没有正确地等待它。
#include <iostream>
#include <thread>
#include <condition_variable>
#include <vector>
#include <atomic>
std::atomic<bool> done{false};
std::atomic<bool> canProduce{true};
std::mutex ready_mutex;
std::condition_variable ready;
std::vector<int> numbers;
void produce() {
auto idx = 0;
while( true ) {
++idx;
std::unique_lock<std::mutex> lk(ready_mutex);
ready.wait(lk, [](){ return canProduce == true; });
for(int i = 0; i < 5; ++i) {
numbers.push_back(i);
}
canProduce = false;
// Manually unlocking thread to ensure that when the consumer thread
// wakes up, this thread is unlocked and the consumer thread can acquire
// the lock, otherwise it'll go back to sleep.
lk.unlock();
ready.notify_one();
if( 5 == idx) {
done = true;
break;
}
}
}
void consume() {
while( true ) {
std::unique_lock<std::mutex> lk(ready_mutex);
ready.wait(lk, [](){ return !numbers.empty(); });
std::cout << "(" << numbers.size() << ") ";
for(auto x: numbers) {
std::cout << x << ", ";
}
numbers.clear();
std::cout << "(" << numbers.size() << ")" << std::endl;
std::cout.flush();
if (done) {
break;
} else {
canProduce = true;
lk.unlock();
ready.notify_one();
}
}
}
int main() {
for (int i = 0; i < 15; i++) {
std::cout << "Attempt " << i << std::endl;
std::thread t(produce);
std::thread t2(consume);
t.join();
t2.join();
done = false;
canProduce = true;
}
return 0;
}
Attempt 1
(5) 0, 1, 2, 3, 4, (0)
(5) 0, 1, 2, 3, 4, (0)
(5) 0, 1, 2, 3, 4, (0)
(5) 0, 1, 2, 3, 4, (0)
(5) 0, 1, 2, 3, 4, (0)
Attempt 2
(5) 0, 1, 2, 3, 4, (0)
(5) 0, 1, 2, 3, 4, (0)
(5) 0, 1, 2, 3, 4, (0)
(5) 0, 1, 2, 3, 4, (0)
(5) 0, 1, 2, 3, 4, (0)
...