Я вижу много описаний и примеров шаблона производителя-потребителя, используя очереди как общий буфер данных. В чем причина использования очереди, а не одного буфера данных элемента?Зачем использовать очередь в качестве коммуникационного буфера между производителем и потребителем?
В каждой реализации производителя-потребителя либо производитель будет производить данные быстрее, чем потребитель может обрабатывать данные, либо производитель будет производить данные медленнее, чем потребитель может обрабатывать данные. Это означает, что очередь в конечном итоге либо заполнится до максимальной емкости, либо будет содержать только один элемент данных, прежде чем этот элемент данных будет потреблен потребителем. Как только очередь будет заполнена, продюсер должен будет ждать пользователя, прежде чем он сможет добавить другой элемент данных. Когда очередь содержит только один элемент, потребитель должен ждать производителя, прежде чем он сможет потреблять другой элемент. Другими словами, очередь будет вести себя как буфер одного элемента.
Обработка очереди требует гораздо больших затрат на обработку и сложность программы, чем работа с одним элементом данных. Должно быть какое-то преимущество, чтобы оправдать добавленную сложность и накладные расходы.
Существует два широких поднабора шаблона производителя-потребителя. Первый - это синхронизированный шаблон производителя-потребителя. Эта модель гарантирует, что потребитель потребляет каждый элемент данных, созданный потребителем, и потребляет его ровно один раз. Синхронизированный шаблон обсуждается выше. Второй - потребитель выборки.
Пользователь выборки потребляет только образец элементов данных, производимых производителем. Если потребитель потребляет элементы данных быстрее, чем производитель создает элементы данных, потребитель, как говорят, перечитывает буфер. Если потребитель потребляет элементы данных медленнее, чем производитель создает элементы данных, потребитель, как говорят, недопонимает элементы данных. Примером конструкции с недостаточной выборкой будет метеорологическая станция, которая генерирует показания температуры со скоростью 100 Гц (100 раз в секунду) при создании отчета со скоростью 1 Гц (1 раз в секунду). Потребителю не нужно считывать 100 точек данных каждую секунду и производить среднее значение этих показаний. Вместо этого он может принимать 1 чтение в секунду и сообщать об этом. Если потребитель составляет только 1% от показаний, нет необходимости предоставлять очередь элементов данных в буфере.
Ограниченные очереди часто используются для управления потреблением памяти. Неограниченные очереди всегда имеют возможность исчерпать память кучи, что может привести к сбою приложения.
Ограниченная многоэлементная очередь может показаться хорошей идеей, когда есть много производителей. Если все данные, производимые многими производителями, должны обрабатываться точно так же, то может быть разумным записать все эти данные в общий буфер, чтобы один или несколько потребителей могли обрабатывать данные. Существует несколько затрат, связанных с многоэлементной очередью. Многоэлементная очередь использует память со скоростью, приблизительно равной размеру каждого элемента данных, умноженному на количество элементов в ограниченной очереди. Для многоэлементной очереди требуется более сложная логика, чем отдельная очередь элементов. Сложность всегда является врагом правильности.
При использовании многоэлементной очереди потребитель всегда считывает элемент данных, который был в очереди самым длинным. Очередь - это, в конце концов, структура данных FIFO. Производители пишут новые элементы в очередь, пока очередь не заполнится. В этот момент либо продюсеры ждут места в очереди, либо перезаписывают новый элемент в очереди. Потребитель по-прежнему читает самый старый элемент в очереди, независимо от того, что делают производители.
Когда очередь заполняет поведение очереди, она похожа на поведение очереди с одним элементом. В блокирующей структуре производителя/потребителя полная очередь заставляет производителя ждать, пока потребитель будет читать из очереди. Очередь отдельных элементов всегда полна или пуста.Полная многоэлементная очередь просто содержит множество элементов данных, ожидающих, что их можно будет использовать, плюс один элемент, который может быть использован.
Очередь отдельных элементов просто исключает время в очереди для ожидающих получения данных.
С потребительской стороны шаблона потребитель может считывать только самый старый элемент в очереди, независимо от того, сколько элементов находится в очереди. Либо элемент доступен для потребителя, либо нет. Размер очереди невидим для потребителя. Если очередь пуста, потребитель должен либо приостановить ожидание доступных данных, либо должен активно пробовать очередь для доступных данных. Для очереди выборки, как упоминалось выше, потребитель может просто обработать самое старое значение в очереди, и очередь никогда не отмечает себя как пустую.
Очень сложно реализовать потребителя выборки с многоэлементной очередью. Будет выполняться одна очередь элементов. Производитель просто перезаписывает все, что находится в очереди, и потребитель читает все, что находится в очереди.
Ниже приведен пример шаблона производителя/потребительского образца, написанного в Аде.
------------------------------------------------------------------
-- Sampling Consumer --
------------------------------------------------------------------
with Ada.Text_IO; use Ada.Text_IO;
procedure Sampling_PC is
protected Buf is
procedure Write(Item : in Integer);
function Read return Integer;
procedure Set_Done;
function Get_Done return Boolean;
private
Value : Integer := Integer'First;
Is_Done : Boolean := False;
end Buf;
protected body Buf is
procedure Write(Item : in Integer) is
begin
Value := Item;
end Write;
function Read return Integer is
begin
return Value;
end Read;
procedure Set_Done is
begin
Is_Done := True;
end Set_Done;
function Get_Done return Boolean is
begin
return Is_Done;
end Get_Done;
end Buf;
task Consumer;
task body Consumer is
begin
while not Buf.Get_Done loop
Put_Line("Consumer read" & Integer'Image(Buf.Read));
end loop;
end Consumer;
begin
for I in 1..10 loop
Put_Line("Producer writing" & Integer'Image(I));
Buf.Write(I);
end loop;
Buf.Set_Done;
end Sampling_PC;
Может потребоваться несколько слов для тех, кто не знаком с заданием Ады. В приведенном выше примере Buf является защищенным объектом. В терминах Ada защищенный объект является объектом, используемым в качестве общего буфера между задачами (подобно потокам). Каждый буфер реализует методы доступа к своим внутренним элементам данных. Виды методов - это процедуры, записи и функции. Процедура имеет безусловный доступ для чтения/записи к защищенному объекту. Каждая процедура автоматически управляет блокировкой чтения/записи защищенного объекта. Запись очень похожа на процедуру, за исключением того, что она добавляет условие управления, подобно переменной условия в потоках. Запись не только реализует блокировку чтения/записи для предотвращения множественных одновременных писателей и перекрывает операции чтения/записи, но также реализует очередь для задач, ожидающих, что условие станет истинным. Функция для защищенного объекта обеспечивает доступ только для чтения к защищенному объекту. Функции автоматически управляют разделяемыми блокировками чтения, так что несколько задач могут одновременно считываться с защищенного объекта. Одновременные чтения не могут повредить защищенный объект.
В приведенном выше примере используются только процедуры и функции. Задача «Потребитель» считывает защищенный объект с помощью функций, в то время как производитель, который является основной задачей программы, в этом случае записывает защищенный объект с помощью процедур.
Пример шаблона производитель-потребитель, используя множество производителей и множество потребителей:
------------------------------------------------------------------
-- Multiple producers and consumers sharing the same buffer --
------------------------------------------------------------------
with Ada.Text_IO; use Ada.Text_Io;
procedure N_Prod_Con is
protected Buffer is
Entry Write(Item : in Integer);
Entry Read(Item : Out Integer);
private
Value : Integer := Integer'Last;
Is_New : Boolean := False;
end Buffer;
protected body Buffer is
Entry Write(Item : in Integer) when not Is_New is
begin
Value := Item;
Is_New := True;
end Write;
Entry Read(Item : out Integer) when Is_New is
begin
Item := Value;
Is_New := False;
end Read;
end Buffer;
task type Producers(Id : Positive) is
Entry Stop;
end Producers;
task body Producers is
Num : Positive := 1;
begin
loop
select
accept Stop;
exit;
or
delay 0.0001;
end select;
Put_Line("Producer" & Integer'Image(Id) & " writing" & Integer'Image(Num));
Buffer.Write(Num);
Num := Num + 1;
end loop;
end Producers;
task type Consumers(Id : Positive) is
Entry Stop;
end Consumers;
task body Consumers is
Num : Integer;
begin
loop
select
accept stop;
exit;
or
delay 0.0001;
end select;
Buffer.Read(Num);
Put_Line("Consumer" & Integer'Image(ID) & " read" & Integer'Image(Num));
end loop;
end Consumers;
P1 : Producers(1);
P2 : Producers(2);
P3 : Producers(3);
C1 : Consumers(1);
C2 : Consumers(2);
C3 : Consumers(3);
begin
delay 0.2;
P1.Stop;
P2.Stop;
P3.Stop;
C1.Stop;
C2.Stop;
C3.Stop;
end N_Prod_Con;
Модель производителя-потребителя также относится к асинхронному потреблению данных. Случай, когда происходит снижение активности производителя или повышение активности потребителей, является наилучшим примером этого шаблона, потому что в этом случае, если в очереди нет данных, тогда потребительские потоки будут ждать, пока производители будут создавать элементы данных. Худший случай шаблона - когда производитель имеет шип. Хотя пул потоков может быть использован, существует ограничение на количество потоков в пуле. –
Поскольку дизайн приложения всегда выполняется для обработки сценариев наихудшего случая, которые в структуре производителя-потребителя являются всплесками активности производителя или снижения активности потребителей, очередь является лучшим выбором. –
Если дизайн приложения всегда выполняется для обработки сценариев наихудшего случая, тогда ограниченные очереди никогда не будут использоваться, потому что худший случай заполняет ограниченную очередь. С другой стороны, динамически распределенные очереди всегда терпят неудачу в худшем случае, когда память исчерпана. В конечном итоге можно обнаружить, что использование файлов обеспечивает большую гибкость, чем использование очередей. –