我有一个非常典型的生产者/消费者问题,已经用bounded buffer解决了。单个进程生成项目并将其交给N个工作线程。辅助线程处理这些项目并将结果放置在有界缓冲区中。最终消费者流程从缓冲区中检索成品。以下数据流程图说明:
每个工作人员需要花费可变的时间来处理其项目,因此工作人员会以基本上随机的顺序将完成的项目插入边界缓冲区。这已经足够好了,但是有时有必要按照它们最初生成的 same 顺序检索完成的项目。所以问题是:
如何修改现有实现以按顺序检索完成的商品?
一个重要的附加约束是,我们必须尊重有界缓冲区的大小。如果缓冲区的大小为M,那么在任何给定时间等待消费者使用的成品不得超过M。
绑定缓冲区
有界缓冲区具有直接接口:
template <class T> class bounded_buffer
{
public:
// initializes a new buffer
explicit bounded_buffer(size_t capacity);
// pushes an item into the buffer, blocks if full
void push(T item);
// pops an item from the buffer, blocks if empty
T pop();
};
处理项目
工作线程使用以下代码来处理项目:
std::unique_lock guard{ source_lock };
auto item = GetNextItem();
guard.unlock();
buffer.push(ProcessItem(std::move(item)));
(实际代码要复杂得多,因为它必须处理输入数据的结尾,取消和处理错误。但是这些细节与问题无关。)
用于检索完成项的代码只是弹出有界缓冲区:
auto processed_item = buffer.pop();
答案 0 :(得分:1)
我将提出两种解决方案。首先是快速和简单。第二种方法建立在第一种方法后面的思想之上,可以产生更高的效率。
第一种方法:std :: future
基本思想是,当我们第一次获取一个值并在完成该项目的处理时将其填充时,我们将在边界缓冲区中“保留”一个空间。 std::future
提供了一种现成的机制来完成此任务。代替使用bounded_buffer<T>
,我们将使用bounded_buffer<std::future<T>>
。我们按如下方式调整工作程序代码:
std::unique_lock guard{ source_lock };
auto item = GetNextItem();
std::promise<T> processed_item;
buffer.push(processed_item.get_future());
guard.unlock();
processed_item.set_value(ProcessItem(std::move(item)));
然后,我们只需轻轻调整一下消费者代码即可从未来获取价值:
auto processed_item = buffer.pop().get();
如果消费者进程在工作人员完成某件商品之前就对其进行了检索,那么std::future<T>::get
将确保消费者阻塞直到该物品准备就绪。
优点:
缺点:
std::future
相对较重,需要额外的内存分配和内部同步。GetNextItem()
昂贵,则可能会出现问题。第二种方法:建立更好的缓冲区
要解决第一种方法中的性能问题,我们可以调整有界缓冲区的实现,以建立在其中保留空间的想法。我们将对其界面进行三处更改:
replace
方法,该方法接受一个定位符和一个值。修改后的界面如下:
template <class T, class P> class bounded_buffer
{
public:
using locator_type = /* unspecified */;
// initializes a new buffer; an item is "available" if and only if it
// satisfies this predicate
explicit bounded_buffer(size_t capacity, P predicate);
// pushes an item into the buffer, blocks if full; the buffer's count of
// available items will increase by one if and only if all items in the
// buffer (including the new one) are available
locator_type push(T item);
// pops an item from the buffer, blocks if empty
T pop();
// replaces an existing item in the buffer; if the item is the first in the
// buffer, then we set the count of available items as follows: 0 if the
// item is unavailable, or X if it is available where X is the number of
// available items at the front of the buffer
void replace(locator_type location, T item);
};
然后,将存储在有界缓冲区中的类型从T
更改为std::variant<std::monostate, T>
。如果谓词包含T,则谓词将认为它“可用”。我们按如下方式更改工作程序代码:
std::unique_lock guard{ source_lock };
auto item = GetNextItem();
auto location = buffer.push(std::monostate{});
guard.unlock();
buffer.replace(location, ProcessItem(std::move(item));
使用者中的检索代码也必须更改,以便从变体中检索值:
auto processed_item = std::get<1>(buffer.pop());
优点:
std::future
方法更轻巧,因此性能更高。 (存储std::variant
索引只需要比原始版本稍多的内存。)future
版本基本相同的方式解决问题。缺点:
错误处理
为简单起见,我省略了错误处理。然而,两种方法都需要适当的异常处理。如果在处理带有已编写代码的项目时发生异常,则消费者将挂起,因为它将等待永远不会到达的保留项目。
答案 1 :(得分:0)
这是我的建议:
在WorkItem
类中添加一个“ requires”字段。该字段的类型为shared_ptr<WorkItem>
(或类似名称)。如果为非NULL,则此字段指示两个WorkItem
之间的依赖关系-例如,如果WorkItem
B的require字段设置为指向WorkItem
A,则意味着使用者进程需要在A
之前消耗B
。
还向每个WorkItem
添加一个condition variable
(及其关联的mutex
)
还向每个WorkItem
添加一个布尔“已消耗”字段。该字段默认为false
,但是,当使用者进程使用WorkItem
时,它将锁定WorkItem
的{{1}},并将此字段设置为{{1} },在mutex
的{{1}}上调用true
,然后解锁notify_all()
。
当工作进程完成对WorkItem
的处理时,它必须检查condition variable
的“ requires”字段。如果“ requires”字段为mutex
,则WorkItem
可能会立即添加到绑定队列中,并完成工作进程的工作。
否则,工作进程必须锁定“ requires”字段的WorkItem
的{{1}}并检查其“ consumed”变量-如果将其设置为NULL
,工作进程应该解锁WorkItem
并加入引用mutex
,工作就完成了。
如果我们到了这里,那么工作进程就无法加入其WorkItem
队列,因为它的true
具有阻止它的排序依赖性。因此,在这种情况下,工作进程应在其依赖项的mutex
上调用WorkItem
。这将使工作进程进入睡眠状态,直到消耗掉它的“ requires”-WorkItem
为止。此时,工作进程将被唤醒(通过步骤3中的WorkItem
调用),并且可以照常将自己的wait()
排入队列。
该逻辑应足以确保在指定时正确排序,同时仍允许工作流程在condition variable
上没有消耗排序要求的情况下尽可能高效地工作。