Я представлю два решения.Первый быстрый и простой.Вторая основывается на идее, лежащей в основе первой, чтобы получить что-то более эффективное.
Первый подход: 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()
стоит дорого.
Второй подход: создать лучший буфер
Для решения проблем производительности в первомподход, мы можем настроить реализацию ограниченного буфера, чтобы встроить идею резервирования пространства в нем.Мы сделаем три изменения в его интерфейсе:
- Изменим конструктор для принятия предиката.
- Изменим метод push, чтобы вернуть локатор.
- Добавить новый
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
.
Минусы:
- Требуются изменения в реализации ограниченного буфера, и его базовые операции больше не соответствуют ожидаемым для этой абстракции.
- Не устраняет выявленную проблему блокировки источникавыше.
Обработка ошибок
Я упустил обработку ошибок для простоты.Тем не менее, для обоих подходов требуется правильная обработка исключений.Если во время обработки элемента с написанным кодом возникнет исключительная ситуация, потребитель будет зависать, потому что он будет ожидать зарезервированного элемента, который никогда не будет доставлен.