Подтвердить что ты не робот

Какой пакет многопоточности для Lua "просто работает" как отправлен?

Кодирование в Lua, у меня трижды вложенный цикл, который проходит через 6000 итераций. Все 6000 итераций являются независимыми и могут быть легко распараллелены. Какой пакет потоков для Lua компилируется из коробки и получает приличные параллельные ускорения на четырех или более ядрах?

Вот что я знаю до сих пор:

  • luaproc происходит от основной команды Lua, но комплект программного обеспечения на luaforge является старым, а в списке рассылки есть отчеты из этого segfault. Кроме того, мне не очевидно, как использовать скалярную модель передачи сообщений для получения результатов в конечном итоге в родительский поток.

  • Lua Lanes предлагает интересные заявки, но, похоже, это тяжеловесное и сложное решение. Многие сообщения в списке рассылки сообщают о том, как Lua Lanes будет создавать или работать на них. Мне самому было трудно получить основной механизм распределения "Lua rocks" для меня.

  • LuaThread требует явной блокировки и требует, чтобы связь между потоками была опосредована глобальными переменными, которые защищены блокировками. Я мог представить себе хуже, но я был бы более счастлив с более высоким уровнем абстракции.

  • Параллельный Lua обеспечивает привлекательную модель передачи сообщений, похожую на Erlang, но она говорит, что процессы не обмениваются памятью. Неясно, работает ли spawn с любой функцией Lua или существуют ограничения.

  • Russ Cox предложил случайную нить, которая работает только для потоков C. Не полезно для меня.

Я отвечу на все ответы, которые сообщают о фактическом опыте, с этими или любым другим многопоточным пакетом или любым ответом, предоставляющим новую информацию.


Для справки, вот цикл, который я хотел бы распараллелить:

for tid, tests in pairs(tests) do
  local results = { }
  matrix[tid] = results
  for i, test in pairs(tests) do
    if test.valid then
      results[i] = { }
      local results = results[i]
      for sid, bin in pairs(binaries) do
        local outcome, witness = run_test(test, bin)
        results[sid] = { outcome = outcome, witness = witness }
      end
    end
  end
end

Функция run_test передается как аргумент, поэтому пакет может быть полезен для меня, только если он может запускать произвольные функции параллельно. Моя цель достаточно parallelism, чтобы получить 100% загрузку процессора на 6-8 ядер.

4b9b3361

Ответ 1

Норман писал о luaproc:

"мне не кажется очевидным, как использовать скалярную модель передачи сообщений для получения результатов в конечном итоге в родительский поток

У меня была та же проблема с вариантом использования, с которым я имел дело. Мне понравился lua proc из-за его простой и легкой реализации, но в моем случае был C-код, вызывающий lua, который запускал совместную процедуру, которая должна была отправлять/получать сообщения для взаимодействия с другими потоками luaproc.

Чтобы достичь желаемой функциональности, мне пришлось добавлять функции в luaproc, чтобы разрешать отправку и получение сообщений из родительского потока или любого другого потока, не запущенного из планировщика luaproc. Кроме того, мои изменения позволяют использовать luaproc send/receive из сопрограмм, созданных из luaproc.newproc(), созданных состояний lua.

Я добавил дополнительную функцию luaproc.addproc() в api, которая должна быть вызвана из любого состояния lua, запущенного из контекста, не управляемого планировщиком luaproc, чтобы настроить себя с помощью luaproc для отправки/получения сообщений.

Я рассматриваю возможность публикации источника в качестве нового проекта github или обращения к разработчикам и выяснения, хотят ли они приложить мои дополнения. Предложения о том, как я должен сделать это доступным для других, приветствуются.

Ответ 2

Проверьте библиотеку threads в семействе факелов. Он реализует модель пула потоков: сначала создаются несколько реальных потоков (pthread в linux и windows thread в win32). Каждый поток имеет объект lua_State и очередь выполнения блокировки, которая допускает задания, добавленные из основного потока.

Объекты Lua копируются из основного потока в поток заданий. Однако объекты C, такие как тензоры факела или tds данные структуры могут быть переданы в потоки заданий через указатели - так достигается ограниченная общая память.

Ответ 3

Я понимаю, что это не готовое решение, но, может, пойти в старую школу и поиграть с вилками? (Предполагая, что вы находитесь в системе POSIX.)

Что я сделал бы:

  • Прямо перед вашим циклом поместите все тесты в очередь, доступные между процессами. (Файл, Redis LIST или все, что вам больше всего нравится.)

  • Также перед циклом появляются несколько вилок с lua-posix (так же, как количество ядер или даже больше, в зависимости от характера тестов). В родительской вилке подождите, пока все дети не уйдут.

  • В каждом fork в цикле получить тест из очереди, выполнить его, поместить результаты где-нибудь. (В файл, в Redis LIST, в любом другом месте, которое вам нравится.) Если в очереди больше нет тестов, закройте.

  • В родительской выборке и обработке всех результатов теста, как и сейчас,

Это предполагает, что параметры и результаты тестирования могут быть сериализованы. Но даже если это не так, я считаю, что обманывать это довольно легко.

Ответ 4

Это прекрасный пример MapReduce

Вы можете использовать LuaRings, чтобы выполнить ваши потребности в распараллеливании.

Ответ 5

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

Обновление

Параллельная Lua, похоже, обрабатывает первоклассные функции и замыкания без заминки. См. Следующую примерную программу.

require 'concurrent'

local NUM_WORKERS = 4       -- number of worker threads to use
local NUM_WORKITEMS = 100   -- number of work items for processing

-- calls the received function in the local thread context
function worker(pid)
    while true do
        -- request new work
        concurrent.send(pid, { pid = concurrent.self() })
        local msg = concurrent.receive()

        -- exit when instructed
        if msg.exit then return end

        -- otherwise, run the provided function
        msg.work()
    end
end

-- creates workers, produces all the work and performs shutdown
function tasker()
    local pid = concurrent.self()

    -- create the worker threads
    for i = 1, NUM_WORKERS do concurrent.spawn(worker, pid) end

    -- provide work to threads as requests are received
    for i = 1, NUM_WORKITEMS do
        local msg = concurrent.receive()

        -- send the work as a closure
        concurrent.send(msg.pid, { work = function() print(i) end, pid = pid })
    end

    -- shutdown the threads as they complete
    for i = 1, NUM_WORKERS do
        local msg = concurrent.receive()
        concurrent.send(msg.pid, { exit = true })
    end
end

-- create the task process
local pid = concurrent.spawn(tasker)

-- run the event loop until all threads terminate
concurrent.loop()

Обновление 2

Поцарапайте все это выше. Что-то не выглядело правильно, когда я тестировал это. Оказывается, Concurrent Lua не является одновременным. "Процессы" реализованы с сопрограммами и все работают совместно в одном контексте потока. Это то, что мы получаем за то, что не читаем внимательно!

Итак, по крайней мере, я устранил один из вариантов, я думаю.: (

Ответ 6

Теперь я построил параллельное приложение, используя luaproc. Вот некоторые заблуждения, которые мешали мне принять его раньше и как их обойти.

  • Как только параллельные потоки запускаются, насколько я могу судить, нет возможности для них вернуться к родительскому. Это свойство было для меня большим блоком. В конце концов я понял путь вперед: когда он делал forking нити, родитель останавливается и ждет. Задание, которое было бы сделано родителем, должно выполняться дочерним потоком, который должен быть посвящен этой работе. Не отличная модель, но она работает.

  • Связь между родителями и детьми очень ограничена. Родитель может передавать только скалярные значения: строки, булевы и числа. Если родитель хочет передать более сложные значения, такие как таблицы и функции, он должен закодировать их как строки. Такое кодирование может выполняться встроенным в программу, или (особенно) функции могут быть помещены в файловую систему и загружены в дочерний элемент с помощью require.

  • Дети ничего не наследуют родительскую среду. В частности, они не наследуют package.path или package.cpath. Мне пришлось обойти это, кстати, я написал код для детей.

  • Самый удобный способ связи от родителя к ребенку - это определить дочерний элемент как функцию и получить родительскую информацию для детей в своих свободных переменных, известных в языках Lua как "upvalues". Эти свободные переменные могут не быть глобальными переменными, и они должны быть скалярами. Тем не менее, это приличная модель. Вот пример:

    local function spawner(N, workers)
      return function()
        local luaproc = require 'luaproc'
        for i = 1, N do
          luaproc.send('source', i)
        end
        for i = 1, workers do
          luaproc.send('source', nil)
        end
      end
    end
    

    Этот код используется как, например,

    assert(luaproc.newproc(spawner(randoms, workers)))
    

    Этот вызов показывает, как значения randoms и workers передаются от родителя к дочернему.

    Это утверждение важно здесь, как если бы вы забыли правила и случайно захватили таблицу или локальную функцию, luaproc.newproc завершится с ошибкой.

Как только я понял эти свойства, luaproc действительно работал "из коробки", когда скачан из askyrme on github.

ETA: существует раздражающее ограничение: в некоторых случаях вызов fread() в одном потоке может препятствовать планированию других потоков. В частности, если я запустил последовательность

local file = io.popen(command, 'r')
local result = file:read '*a'
file:close()
return result

Операция read блокирует все остальные потоки. Я не знаю, почему это... Я предполагаю, что это какая-то глупость происходит внутри glibc. Обходной путь, который я использовал, заключался в том, чтобы напрямую обращаться к read(2), что требовало небольшого кода клея, но это работает правильно с io.popen и file:close().

Есть еще одно ограничение:

  • В отличие от оригинальной концепции обмена сообщениями Tony Hoare о передаче последовательной обработки и в отличие от большинства зрелых, серьезных реализаций передачи синхронных сообщений, luaproc не позволяет приемнику блокировать несколько каналов одновременно. Это ограничение является серьезным, и он исключает многие шаблоны проектирования, в которых происходит синхронная передача сообщений, но он по-прежнему находит для многих простых моделей parallelism, особенно "парфегин", который мне нужно решить для моего оригинала проблема.