0Pricing
Lua Academy · Урок

Шаблон «производитель—потребитель»

Реализуйте конвейер «производитель—потребитель» с помощью сопрограмм.

«Шаблон «производитель—потребитель»» — бесплатный урок Lua Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Lua Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Lua Academy содержит 4 уроков всего.

Объяснение шаблона

Шаблон «производитель — потребитель» с корутинами: производитель создаёт данные и выдаёт каждый элемент через yield, а потребитель получает элементы по одному. Ни одному из них не нужно знать устройство другого — они обмениваются данными через yield/resume. Это отделяет создание данных от их обработки.

local function makeProducer(items)
  return coroutine.create(function()
    for _, item in ipairs(items) do
      coroutine.yield(item)
    end
  end)
end

local function consume(producer)
  while true do
    local ok, item = coroutine.resume(producer)
    if not ok or item == nil then break end
    print("Processing:", item)
  end
end

consume(makeProducer({10, 20, 30, 40}))

Бесконечный производитель

Корутина-производитель может выдавать бесконечную последовательность. Потребитель управляет моментом остановки. Это идеально подходит для потоковых источников данных: показаний датчиков, событий журнала и генерируемых последовательностей.

local function primes()
  return coroutine.wrap(function()
    local function isPrime(n)
      if n<2 then return false end
      for i=2, math.floor(math.sqrt(n)) do
        if n%i==0 then return false end
      end
      return true
    end
    local n = 2
    while true do
      if isPrime(n) then coroutine.yield(n) end
      n = n + 1
    end
  end)
end

local gen = primes()
for i = 1, 10 do
  io.write(gen() .. " ")
end
print()  -- 2 3 5 7 11 13 17 19 23 29

Потребитель как корутина

Потребитель тоже может быть корутиной, образуя полностью симметричную пару производителя и потребителя. Третья функция — «диспетчер» — координирует их работу. Это полезно для буферизованных конвейеров, в которых производитель и потребитель работают с разной скоростью.

local function producer()
  return coroutine.create(function(consumer)
    for i = 1, 5 do
      coroutine.resume(consumer, i * 10)
      coroutine.yield()  -- wait
    end
  end)
end

local consumer = coroutine.create(function()
  while true do
    local val = coroutine.yield()
    print("Received:", val)
  end
end)

coroutine.resume(consumer)  -- start, waits for data
local pro = producer()
for i = 1, 5 do
  coroutine.resume(pro, consumer)
end

Обратное давление

Обратное давление возникает, когда потребитель сообщает производителю, что нужно снизить скорость. С корутинами это происходит естественно: производитель выполняет yield после каждого элемента и ждёт, пока потребитель запросит следующий. Если потребитель работает медленно, производитель просто ждёт.

local function slowConsumer(gen)
  for v in gen do
    -- Simulate slow processing
    print("Processing", v, "...")
    -- In real code: os.execute("sleep 0.1")
  end
end

local function fastProducer(n)
  return coroutine.wrap(function()
    for i = 1, n do
      print("Producing", i)
      coroutine.yield(i)
    end
  end)
end

-- Producer automatically waits for consumer:
slowConsumer(fastProducer(4))

Пакетная обработка

Производитель, работающий пакетами, собирает N элементов, прежде чем выдать пакет. Потребитель получает и обрабатывает целые пакеты. Это уменьшает накладные расходы на координацию у производителей с высокой частотой выдачи данных.

local function batchProducer(items, batchSize)
  return coroutine.wrap(function()
    local batch = {}
    for _, item in ipairs(items) do
      batch[#batch+1] = item
      if #batch >= batchSize then
        coroutine.yield(batch)
        batch = {}
      end
    end
    if #batch > 0 then coroutine.yield(batch) end
  end)
end

local data = {1,2,3,4,5,6,7,8,9,10}
for batch in batchProducer(data, 3) do
  print("Batch:", table.concat(batch, ","))
end
-- Batch: 1,2,3
-- Batch: 4,5,6
-- Batch: 7,8,9
-- Batch: 10

Производитель строк файла

Читайте файл лениво: корутина-производитель считывает строки по одной и выдаёт каждую через yield. Потребитель обрабатывает строки, не загружая весь файл в память.

local function lineProducer(path)
  return coroutine.wrap(function()
    local f = io.open(path, "r")
    if not f then return end
    for line in f:lines() do
      coroutine.yield(line)
    end
    f:close()
  end)
end

-- Process file line by line without loading all at once
local wordCount = 0
for line in lineProducer("data.txt") do
  for _ in line:gmatch("%S+") do
    wordCount = wordCount + 1
  end
end
print("Words:", wordCount)

Этап преобразования

Добавьте этап преобразования между производителем и потребителем. Преобразователь считывает данные из одной корутины и передаёт обработанные значения другой. Создавайте конвейеры данных, соединяя такие этапы.

local function transform(source, fn)
  return coroutine.wrap(function()
    for v in source do
      coroutine.yield(fn(v))
    end
  end)
end

local numbers = coroutine.wrap(function()
  for i = 1, 6 do coroutine.yield(i) end
end)

local doubled = transform(numbers, function(n) return n*2 end)
local filtered = coroutine.wrap(function()
  for v in doubled do
    if v > 4 then coroutine.yield(v) end
  end
end)

for v in filtered do io.write(v.." ") end
print()  -- 6 8 10 12

Передача ошибок в конвейерах

Ошибки в конвейерах корутин передаются через состояние завершения или с помощью pcall. Оборачивайте этапы конвейера в pcall, чтобы перехватывать ошибки и корректно обрабатывать их, не нарушая работу всего конвейера.

local function safeStage(source, fn)
  return coroutine.wrap(function()
    for v in source do
      local ok, result = pcall(fn, v)
      if ok then
        coroutine.yield(result)
      else
        print("Error processing", v, ":", result)
      end
    end
  end)
end

local data = coroutine.wrap(function()
  for _, v in ipairs({4, -1, 9, 0, 16}) do coroutine.yield(v) end
end)

local results = safeStage(data, function(n)
  assert(n > 0, "non-positive")
  return math.sqrt(n)
end)

for v in results do print(v) end

Объединение производителей

Объедините нескольких производителей в одного: по очереди обращайтесь ко всем активным производителям и выдавайте по одному элементу от каждого. Остановитесь, когда все производители исчерпают данные.

local function merge(...)
  local producers = {...}
  return coroutine.wrap(function()
    while #producers > 0 do
      local alive = {}
      for _, co in ipairs(producers) do
        local ok, v = coroutine.resume(co)
        if ok and v ~= nil then
          coroutine.yield(v)
          alive[#alive+1] = co
        end
      end
      producers = alive
    end
  end)
end

local function src(items)
  return coroutine.create(function()
    for _,v in ipairs(items) do coroutine.yield(v) end
  end)
end

for v in merge(src({1,3,5}),src({2,4,6})) do
  io.write(v.." ")
end
print()  -- 1 2 3 4 5 6

Практический пример

Конвейер обработки журналов: производитель считывает строки журнала, этап фильтрации оставляет только строки с ошибками, этап преобразования извлекает ключевую информацию, а потребитель группирует данные по типу ошибки. Каждый этап — независимая корутина, которую можно тестировать отдельно.

local function makeFilter(src, pred)
  return coroutine.wrap(function()
    for line in src do
      if pred(line) then coroutine.yield(line) end
    end
  end)
end

local logs = coroutine.wrap(function()
  local entries = {
    "INFO: started",
    "ERROR: timeout",
    "DEBUG: connecting",
    "ERROR: auth failed",
  }
  for _, e in ipairs(entries) do coroutine.yield(e) end
end)

local errors = makeFilter(logs, function(l) return l:match("^ERROR") end)
for line in errors do print(line) end
-- ERROR: timeout
-- ERROR: auth failed

Быстрая проверка

Как шаблон «производитель — потребитель» на основе корутин обрабатывает обратное давление?

Итоги: производитель и потребитель

Краткое содержание:

  • Производитель выдаёт элементы; потребитель управляет им, вызывая resume
  • Бесконечные производители: while true do yield() end
  • Конвейеры: соединяйте корутины преобразования между производителем и потребителем
  • Обратное давление возникает автоматически: производитель ждёт потребителя
  • Используйте обёртки-корутины для этапов пакетной обработки, фильтрации и объединения
  • Применяйте pcall на этапах конвейера для изоляции ошибок

Часто задаваемые вопросы

Урок «Шаблон «производитель—потребитель»» бесплатный?

Да — полный текст урока «Шаблон «производитель—потребитель»» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Lua Academy, подпишись на CoddyKit PRO. Курс Lua Academy содержит 4 уроков всего.

Чему я научусь в уроке «Шаблон «производитель—потребитель»»?

Реализуйте конвейер «производитель—потребитель» с помощью сопрограмм. Ты практикуешь Lua Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Lua Academy?

Предыдущий опыт не требуется. Lua Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Шаблон «производитель—потребитель»»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Lua Academy?

Да. Каждый урок Lua Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Создание сопрограмм
  2. resume и yield
  3. Состояние сопрограммы
  4. Шаблон «производитель—потребитель»
← Назад к Lua Academy