我需要实现一个特殊的生产者-消费者方案,其中类Consumer创建两个处理两个端口的线程,每个线程将一个值存储到相应的端口中,如果另一个端口上有值,则{{1}调用}消耗两个端口上的值。这是我的尝试:
process
以下是输出:
#include <thread>
#include <atomic>
#include <condition_variable>
#include <vector>
#include <iostream>
#include <mutex>
struct Port {
int value;
std::atomic<bool> free;
};
class Consumer {
private:
Port port1;
Port port2;
std::mutex mutex;
std::condition_variable port1_ready;
std::condition_variable port2_ready;
std::vector<std::thread> workers;
std::atomic<bool> done;
int count;
public:
Consumer() : done(false), count(0) {
port1.free = true;
port1.value = 0;
port2.free = true;
port1.value = 0;
workers.push_back(std::thread([this]{
while (!done) {
write1(rand());
}
}));
workers.push_back(std::thread([this]{
while (!done) {
write2(rand());
}
}));
}
void write1(int value) {
std::unique_lock lock(mutex);
port1.value = value;
port1.free = false;
std::cout << "port1 stored " << value << std::endl;
port1_ready.notify_one();
if (port2.free) {
port2_ready.wait(lock);
}
process("port1");
}
void write2(int value) {
std::unique_lock lock(mutex);
port2.value = value;
port2.free = false;
std::cout << "port2 stored " << value << std::endl;
port2_ready.notify_one();
if (port1.free) {
port1_ready.wait(lock);
}
process("port2");
}
void process(std::string string) {
port1.free = true;
port2.free = true;
std::cout << string << " consumed " << port1.value << " " << port2.value << std::endl;
if (count++ == 20) done = true;
}
~Consumer() {
for (auto& w: workers) {
w.join();
}
}
};
int main(int argc, char** argv) {
Consumer c{};
return 0;
}
有时它将成功返回,而其他则会陷入僵局。
答案 0 :(得分:1)
注意以下几点可以很容易地发现您的逻辑错误:
void process(std::string string) {
port1.free = true;
port2.free = true;
这清楚地表明,您的意图是考虑现在放置在两个“端口”中要进行“处理”的值。也就是说,一旦在两个“端口”中都放置了一个值,则两个值都将被“处理”,并且两个端口都将再次“空闲”。
但是,请注意日志的开头:
port1 stored 41
port2 stored 41
port2 consumed 41 41
到目前为止,两个端口中均放置了41个端口,port2
进程最终对其进行了“处理”。但是之后:
port2 stored 18467
port1 consumed 41 18467
在这一点上,事情几乎已经脱离了轨道。 41已经被“处理”了,显然不应该再次“处理”。
在两张纸上打印write1()
和write2()
的内容。使用左手的食指来跟踪write1()
线程的执行,并用右手的食指来跟踪write2()
线程的执行。
从您的右手开始,跟踪write2()
,因为它锁定了互斥锁并开始了业务,并发现了这个
if (port2.free) {
是真的,然后
port2_ready.wait(lock);
等待该条件变量。这样可以解除互斥锁的阻塞,并且您的左手食指现在可以向前移动。现在,您的左手食指向前移动,直到:
port2_ready.notify_one();
这会通知另一个线程,该线程必须等待互斥锁被解锁,因此它在右手食指继续移动时等待:
if (port1.free) {
这是真的,因此现在第一个线程可以移动了。而且,如果您继续进行下去,您将看到两个线程如何最终进入process()
,而不仅仅是其中一个。失败。
此逻辑从根本上被破坏了。有几种正确执行此操作的方法,但是最简单的方法如下。当任一线程获取互斥锁时,它就会
检查线程拥有的端口中是否已经有值(从上次被调用开始)。
如果端口已经有一个值,请等待条件变量,直到该端口空闲为止(依靠另一个线程将其清除)。
如果端口空闲,则将其值保存在线程的端口中,然后检查另一个线程的端口是否已具有值。如果没有,则该线程可以返回并继续其业务以获取下一个值,并确保另一个线程将处理两个保存的值。
否则,两个端口都有值,调用process()
来处理它们,清除两个端口,并用信号通知另一个线程的条件变量,让它知道如果它正在等待其端口空闲,则它是现在免费用于保存另一个线程的下一个值。
答案 1 :(得分:1)
即使process
和port1.free
中的一个为真,您也会呼叫port2.free
。
您可以将代码更改为:
struct Port {
std::optional<int> value;
};
class Consumer {
private:
Port port1;
Port port2;
std::mutex mutex;
std::condition_variable port1_ready;
std::condition_variable port2_ready;
std::vector<std::thread> workers;
std::atomic<bool> done;
int count;
public:
Consumer() : done(false), count(0) {
std::random_device rd;
workers.push_back(std::thread([this, gen = std::mt19937{rd()}] () mutable {
while (!done) {
write1(gen());
}
}));
workers.push_back(std::thread([this, gen = std::mt19937{rd()}] () mutable {
while (!done) {
write2(gen());
}
}));
port1_ready.notify_one();
port2_ready.notify_one();
}
void write1(int value) {
std::unique_lock lock(mutex);
port1_ready.wait(lock, [&](){ return !port1.value; });
port1.value = value;
std::cout << "port1 stored " << value << std::endl;
if (port2.value) {
process("port1");
}
}
void write2(int value) {
std::unique_lock lock(mutex);
port2_ready.wait(lock, [&](){ return !port2.value; });
port2.value = value;
std::cout << "port2 stored " << value << std::endl;
if (port1.value) {
process("port2");
}
}
void process(std::string string) {
std::cout << string << " consumed " << *port1.value << " " << *port2.value << std::endl;
port1.value.reset();
port2.value.reset();
port1_ready.notify_one();
port2_ready.notify_one();
if (count++ == 20) done = true;
}
~Consumer() {
for (auto& w: workers) {
w.join();
}
}
};
int main() {
Consumer c{};
}