Какой пакет многопоточности для 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 ядер.
Ответы
Ответ 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, особенно "парфегин", который мне нужно решить для моего оригинала проблема.