Ниже несколько вопросов к поддержке и/или знатокам по теме потоковой безопасности.
Читаем документацию:
Цитата
Одновременная работа с таблицами из функций обратного вызова скрипта и функции main() может приводить к неопределенным ситуациям.
В языке Lua все переменные на самом деле являются ключами таблиц, называемых environments. Присвоение ключу значения nil удаляет его из таблицы. То есть, если в коде написано
Код
var = 5 -- глобальная переменная
function main()
var = nil
end
то это то же самое, что и
Код
_G["var"] = 5 -- глобальная переменная
function main()
_G["var"] = nil
end
(в новых версиях Lua вместо _G теперь _ENV, но не суть). Видно, что функция main() удаляет ключ var из таблицы _G. Вопрос 1: приводит ли присвоение nil глобальным переменным из функции main() (то есть не главного потока) к неопределенным ситуациям?
Читаем документацию дальше:
Цитата
Выполнение потокобезопасной функции блокирует выполнение кода в другом потоке до окончания работы функции.
Вопрос 2: имеется ввиду, что другие потоки блокируются при попытке вызвать (ту же или какую-то другую) потокобезопасную функцию (классическая critical section)? Или же вообще все потоки волшебным образом останавливаются, ну, так, "на всякий случай"? :)
Еще читаем:
Цитата
В таблице представлены стандартные функции Lua и соответствующие им потокобезопасные аналоги: concat sconcat remove sremove insert sinsert sort ssort
Про table.sconcat() вопросов нет (ну хоть что-то понял! :)). Думаем про table.ssort():
Код
local t = {}
function OnSomething(param) -- какой-то callback
table.sinsert(t, param)
end
function main()
table.ssort(t)
for _, v in ipairs(t) do
print(v)
end
end
Как только table.ssort() закончит работу другой поток может добавить новые данные - и у нас опять потенциально не упорядоченная таблица, которую мы и напечатаем. Вопрос 3: как вообще тогда использовать table.ssort()?
Думаем про table.sinsert() и table.sremove(): Вопрос 4: в примере выше, что произойдет, если один поток изменит таблицу потокобезопасной функцией как раз в то время, когда другой поток находится где-то в дебрях интерпретатора Lua (внутри ipairs(), t[key], или еще где)? Что именно сделано, чтобы все не "взорвалось"? Или "так делать нельзя"?
Но предположим, что мы используемтолько потокобезопасные функции и другими способами к таблице не обращаемся, и реализуем классический message passing, то есть очередь сообщений между потоками:
Код
local mqueue = {}
function OnSomething(param) -- какой-то callback
while true do
local msg = table.sremove(mqueue, 1)
if not msg then break end
-- обрабатываем сообщение msg (например, меняем логику работы этого callback)
end
-- обрабатываем param согласно (возможно новой) логике
end
function main()
...
table.sinsert(<сообщение>)
...
end
Вопрос 5: а какие "сообщения" можно посылать таким образом? Абсолютно любые "типы" Lua, или есть ограничения? Этот вопрос связан со следующим:
Вопрос 6: а как потоки дружат со сборщиком мусора (garbage collector)? Он один на все потоки, или по одному на каждый? Если один, то в каком потоке выполняется, и что именно сделано, чтобы GC в одном потоке не "взорвал" другой работающий поток, и не "взорвался" сам, считая свободной память, которую другой поток как раз начинает использовать)? Если же GC у каждого потока свой, то в случае message passing как в примере выше как именно передается ownership (то есть чей GC отвечает за рекламацию сообщения)?
Очень надеюсь получить ответы на все вопросы (и в особенности на 6-й) - хочется проникнуться доверием к реализации, управляющей моими активами. :)
kroki написал: Вопрос 1: приводит ли присвоение nil глобальным переменным из функции main() (то есть не главного потока) к неопределенным ситуациям?
А как связано простое присвоение nil с одновременной работой из функций обратного вызова скрипта и функции main()? Вы обыгрываете совершенно разные сценарии, при этом считаете что это одно и тоже. Вот если бы Вы из разных потоков одновременно присваивали nil, тогда да, согласно документации это может привести к неопределенным последствиям. Для избежания такой неопределенности, придуманы функции из главы руководства "Потокобезопасные функции для работы с таблицами Lua"
Цитата
kroki написал: Читаем документацию дальше:ЦитатаВыполнение потокобезопасной функции блокирует выполнение кода в другом потоке до окончания работы функции. Вопрос 2: имеется ввиду, что другие потоки блокируются при попытке вызвать (ту же или какую-то другую) потокобезопасную функцию (классическая critical section)? Или же вообще все потоки волшебным образом останавливаются, ну, так, "на всякий случай"? :)
на наш взгляд фраза "блокирует выполнение кода в другом потоке" вполне понятна и не требует уточнений.
Цитата
kroki написал: Как только table.ssort() закончит работу другой поток может добавить новые данные - и у нас опять потенциально не упорядоченная таблица, которую мы и напечатаем. Вопрос 3: как вообще тогда использовать table.ssort()?
А чего Вы ожидали? что оно само будет где-то параллельно сортироваться после окончания ssort? Нет такого не будет.
Цитата
kroki написал: Вопрос 4: в примере выше, что произойдет, если один поток изменит таблицу потокобезопасной функцией как раз в то время, когда другой поток находится где-то в дебрях интерпретатора Lua (внутри ipairs(), t[key], или еще где)? Что именно сделано, чтобы все не "взорвалось"? Или "так делать нельзя"?
Вы же сами выше процетировали:
Цитата
kroki написал: Выполнение потокобезопасной функции блокирует выполнение кода в другом потоке до окончания работы функции.
Цитата
kroki написал: Вопрос 5: а какие "сообщения" можно посылать таким образом? Абсолютно любые "типы" Lua, или есть ограничения? Этот вопрос связан со следующим:
Если вопрос в том, что можно вставить в таблицу, то это совсем никак не связано с вопросом из п.6.
Цитата
kroki написал: Вопрос 6: а как потоки дружат со сборщиком мусора (garbage collector)? Он один на все потоки, или по одному на каждый? Если один, то в каком потоке выполняется, и что именно сделано, чтобы GC в одном потоке не "взорвал" другой работающий поток, и не "взорвался" сам, считая свободной память, которую другой поток как раз начинает использовать)? Если же GC у каждого потока свой, то в случае message passing как в примере выше как именно передается ownership (то есть чей GC отвечает за рекламацию сообщения)?
Все LUA функции выполняются в том потоке в котором вызваны и не важно garbage collector это или любая другая функция. Потокобезопасные функции отличаются только тем что блокируют другой поток пока сами выполняются и опять же в том потоке где запущены. И garbage collector к ним не относится.
kroki написал: Вопрос 1: приводит ли присвоение nil глобальным переменным из функции main() (то есть не главного потока) к неопределенным ситуациям?
А как связано простое присвоение nil с одновременной работой из функций обратного вызова скрипта и функции main()?
В момент одного из таких присваиваний (то есть удалений) реализация Lua может решить аллоцировать для таблицы меньший кусок памяти, перехэшировать, и пр. Если во время такого передела другой поток обратится к какой-то глобальной переменной, то...
Цитата
Вы обыгрываете совершенно разные сценарии, при этом считаете что это одно и тоже. Вот если бы Вы из разных потоков одновременно присваивали nil, тогда да, согласно документации это может привести к неопределенным последствиям.
По-моему в ваших словах противоречие: вы же не можете знать, делаете ли вы "простое присваивание nil" (которое по-вашему не связано с функцией main()), или "из разных потоков одновременно присваивали nil", которое делать нельзя - вдруг присваивание делается изнутри какой-то функции, которую даже не вы написали. Отсюда:
Ответ 1: присваивать глобальным переменным nil нельзя. Динамически создавать новые глобальные имена также нельзя. Так?
Цитата
Цитата
kroki написал: Читаем документацию дальше: Выполнение потокобезопасной функции блокирует выполнение кода в другом потоке до окончания работы функции.
на наш взгляд фраза "блокирует выполнение кода в другом потоке" вполне понятна и не требует уточнений.
И не делиться сакральным знанием - ваша политика? :) Но я понял: другой поток останавливается, даже если он не пытается вызвать потокобезопасную функцию. Попробую угадать:
Ответ 2: в интерпретатор Lua (видимо, перед выполнением следующей инструкции байт-кода) встроена проверка флага, что другой поток вызывает потокобезопасную функцию. Увидев флаг, интерпретатор останавливается и ждет, пока другой поток выйдет из функции. Конечно, можtт получиться так, что поток уже выполняет код GC, который написан на C и в Lua долго не вернется, флага не увидит. Но это превращает вопрос 2 в вопрос 6, так что этот вопрос снимаю.
Цитата
Цитата
kroki написал: Как только table.ssort() закончит работу другой поток может добавить новые данные - и у нас опять потенциально не упорядоченная таблица, которую мы и напечатаем. Вопрос 3: как вообще тогда использовать table.ssort()?
А чего Вы ожидали? что оно само будет где-то параллельно сортироваться после окончания ssort? Нет такого не будет.
Не уверен, поняли ли вы вопрос (я ожидал только его понимания, больше ничего). Перефразирую:
Вопрос 3: какой смысл в потокобезопасной сортировке, если ее результат (то есть упорядоченный массив) увидеть (то есть получить к нему доступ) никак нельзя? Если увидеть можно, то, пожалуйста, приведите пример кода. Я утверждаю, что после выхода из table.ssort() и перед любым другим действием другой поток может что-то вставить в массив с помощью table.sinsert(), и, таким образом, нарушить его упорядоченность - а тогда зачем было вообще упорядочивать?
Цитата
Цитата
kroki написал: Вопрос 4: в примере выше, что произойдет, если один поток изменит таблицу потокобезопасной функцией как раз в то время, когда другой поток находится где-то в дебрях интерпретатора Lua (внутри ipairs(), t[key], или еще где)? Что именно сделано, чтобы все не "взорвалось"? Или "так делать нельзя"?
Вы же сами выше процетировали:
Цитата
kroki написал: Выполнение потокобезопасной функции блокирует выполнение кода в другом потоке до окончания работы функции.
И там же я сказал, что смысл фразы мне не вполне ясен. Если ответ 2 я понял правильно, то да, вопрос 4 снимается.
Цитата
Цитата
kroki написал: Вопрос 5: а какие "сообщения" можно посылать таким образом? Абсолютно любые "типы" Lua, или есть ограничения? Этот вопрос связан со следующим:
Если вопрос в том, что можно вставить в таблицу, то это совсем никак не связано с вопросом из п.6.
И, следовательно, вопрос не в том, что можно вставить в таблицу ;). Подробнее ниже.
Цитата
Цитата
kroki написал: Вопрос 6: а как потоки дружат со сборщиком мусора (garbage collector)? Он один на все потоки, или по одному на каждый? Если один, то в каком потоке выполняется, и что именно сделано, чтобы GC в одном потоке не "взорвал" другой работающий поток, и не "взорвался" сам, считая свободной память, которую другой поток как раз начинает использовать)? Если же GC у каждого потока свой, то в случае message passing как в примере выше как именно передается ownership (то есть чей GC отвечает за рекламацию сообщения)?
Все LUA функции выполняются в том потоке в котором вызваны и не важно garbage collector это или любая другая функция. Потокобезопасные функции отличаются только тем что блокируют другой поток пока сами выполняются и опять же в том потоке где запущены. И garbage collector к ним не относится.
Раз из разных потоков можно обращаться к одним и тем же глобальным именам, значит разные потоки работают с одним и тем же global environment (точнее, могла быть и копия, но нет, проверял). Если GCзапускаются в каждом потоке свой, то как они не "наступают друг другу на пятки", и не конфликтуют с выполнением другого потока? Например, в одном потоке запустился GC, но работает-то он со всей памятью (раз environment общий). Если никакой синхронизции между потоками нет, то события "объект больше не используется" и "на объект больше никто не ссылается" в общем случае разные потоки могут обозревать в разном порядке, как из-за out of order execution, так и в банальных случаях, когда два GC в разных потоках одновременно увидят, что такая-то таблица более не используется, и одновременно попытаются ее освободить - возможно возникнет бо-бо :). Вопрос 5 вырос из предположения, что разные GC работают каждый на своих данных, которыми он "владеет", и коллизий не бывает. Я более не думаю, что это так (снимаю вопрос 5), но тогда остается:
Вопрос 6: как исключается ситуация коллизии между GC в разных потоках, запущенными одновременно? Не могут два разных GC одновременно начать удалять один и тот же объект?
kroki написал: По-моему в ваших словах противоречие:
По моему это Ваш код противоречит Вашим словам. Еще раз, согласно Вашему коду никакого одновременного присвоения нет. А значит вопрос совершенно никак не связан с цитатой из документации. Либо Ваш пример кода не соответствует вопросу, либо вопрос поставлен некорректно
Не надо искать скрытый смысл там где его просто нет. Фраза "блокирует выполнение кода в другом потоке" означает ровно то что означает без скрытого смысла. И тут нет никакого сокрального знания.
Цитата
kroki написал: Вопрос 3: какой смысл в потокобезопасной сортировке, если ее результат (то есть упорядоченный массив) увидеть (то есть получить к нему доступ) никак нельзя? Если увидеть можно, то, пожалуйста, приведите пример кода. Я утверждаю, что после выхода из table.ssort() и перед любым другим действием другой поток может что-то вставить в массив с помощью table.sinsert(), и, таким образом, нарушить его упорядоченность - а тогда зачем было вообще упорядочивать?
Цель функции ssort выполнить сортировку таблицы не боясь что во время сортировки в таблице изменятся данные. Ровно это функция и делает. Если не видите смысла использовать эту функцию то просто не используйте и все. Никто не заставляет. Если нужно заблокировать остальные потоки во время вывода таблицы, то при чем тут функция ssort?
Цитата
kroki написал: Если GC запускаются в каждом потоке свой, то как они не "наступают друг другу на пятки", и не конфликтуют с выполнением другого потока?
В этом вопросе лучше и правильней обратиться к истокам. garbage collector не является нашей разработкой.
Цитата
kroki написал: Вопрос 6: как исключается ситуация коллизии между GC в разных потоках, запущенными одновременно? Не могут два разных GC одновременно начать удалять один и тот же объект?
В нашем ПО, это никак не исключается. Все работает так как это реализовали авторы Lua. Можете проверить поведение функции поставив ряд экспериментов.
1: приводит ли присвоение nil глобальным переменным из функции main() (то есть не главного потока) к неопределенным ситуациям?
Смотря как написан код
Код
t={}
...
function main()
...
t=nil
...
end
OnSmth()
t.x = 1
t.y = 2
t.z = 3 -- тут может возникнуть ошибка attempt to index global 't' (a nil value)
Цитата
2: имеется ввиду, что другие потоки блокируются при попытке вызвать (ту же или какую-то другую) потокобезопасную функцию (классическая critical section )? Или же вообще все потоки волшебным образом останавливаются, ну, так, "на всякий случай"? :)
Другой "поток" волшебным образом останавливается, если будет обращение к глобальным данным.
Цитата
3: как вообще тогда использовать table.ssort() ?
в описанной Вами ситуации - никак.
Цитата
Думаем про table.sinsert() и table.sremove(): 4: в примере выше, что произойдет, если один поток изменит таблицу потокобезопасной функцией как раз в то время, когда другой поток находится где-то в дебрях интерпретатора Lua (внутри ipairs(), t[key], или еще где)? Что именно сделано, чтобы все не "взорвалось"? Или "так делать нельзя"?
Как и в любых других языках программирования - изменение содержимого контейнера в одном потоке и итерация по элементам в другом может привести к неопределенной ситуации. Данные останутся консистентны, но итераторы (pairs, ipairs) могут работать не так как Вы ожидаете.
Цитата
5: а какие "сообщения" можно посылать таким образом? Абсолютно любые "типы" Lua, или есть ограничения? Этот вопрос связан со следующим:
Не совсем понятно, поясните.
Цитата
6: а как потоки дружат со сборщиком мусора ( garbage collector )? Он один на все потоки, или по одному на каждый? Если один, то в каком потоке выполняется, и что именно сделано, чтобы GC в одном потоке не "взорвал" другой работающий поток, и не "взорвался" сам, считая свободной память, которую другой поток как раз начинает использовать)? Если же GC у каждого потока свой, то в случае message passing как в примере выше как именно передается ownership (то есть чей GC отвечает за рекламацию сообщения)?
Сборщик мусора один. Вообще потоки не являются потоками в "классическом" понимании. Это скорее новый объект с отдельным стеком для вызовов функций и локальных переменных. При этом у них общие глобальные данные. Попытка обращения к ним из одного "потока" блокирует другой, если он тоже обращается к ним.
kroki написал: По-моему в ваших словах противоречие:
По моему это Ваш код противоречит Вашим словам. Еще раз, согласно Вашему коду никакого одновременного присвоения нет. А значит вопрос совершенно никак не связан с цитатой из документации.
В документации не идет речи о присвоении, есть выражение "одновременная работа". То есть не только запись, но и чтение. Потоки не могут не читать global environment: всякий раз, когда вы пишите table.something() или string.something(), вы читаете значения _G["table"] и _G["string"]. Давайте еще раз:
Является ли присвоение переменной значения nil операцией изменения таблицы, в которой переменная хранится? - Да, в Lua присвоение _G["var"] = nil это операция удаления из таблицы.
Обращаются ли разные потоки к таблице глобальных переменных? - Да, как сказано выше, этого не избежать.
Можно ли одновременно менять таблицу из одного потока и читать из другого без блокировок? - Нет, согласно документации (и здравому смыслу) нужно использовать потокобезопасные функции.
Используются ли потокобезопасные функции при обращении к глобальным переменным? - Нет, к глобальным переменным обращаются напрямую.
Отсюда вывод: присваивать глобальным переменным значение nil из потока нельзя. Не знаю, почему сей логический вывод дается с таким трудом.
Цитата
Цитата
kroki написал: Попробую угадать:
Не надо искать скрытый смысл там где его просто нет. Фраза "блокирует выполнение кода в другом потоке" означает ровно то что означает без скрытого смысла. И тут нет никакого сокрального знания.
Возможно играет роль разница в кругозорах. Вы видите ровно один вариант интерпретации и для вас все однозначно. Я вижу несколько вариантов, начиная с того наблюдения, что словом "блокировка" на русский язык переводят английские термины как blocking, так и locking (и это совершенно разные концепции), и заканчивая семантикой русского языка: если вас спрашивают, "вы курите?", то это может означать как "прямо сейчас", так и "вообще по жизни". Поэтому "блокирует выполнение кода в другом потоке" может означать как захват какой-то блокироки (Lock), так и блокирование (остановку) в каком-то месте кода (Blocking); как "прямо сейчас", так и "в момент, когда выполнение потока дойдет до критической секции". Как видите, интерпретаций несколько, и мне хотелось понять, что именно имеется ввиду. Я понял, что вы сделали все, что могли, и ответа не дадите (а я и не утверждаю, что обязаны - вы уже дали достаточно других ответов).
Цитата
Цитата
kroki написал: Вопрос 3: какой смысл в потокобезопасной сортировке, если ее результат (то есть упорядоченный массив) увидеть (то есть получить к нему доступ) никак нельзя? Если увидеть можно, то, пожалуйста, приведите пример кода. Я утверждаю, что после выхода из table.ssort() и перед любым другим действием другой поток может что-то вставить в массив с помощью table.sinsert(), и, таким образом, нарушить его упорядоченность - а тогда зачем было вообще упорядочивать?
Цель функции ssort выполнить сортировку таблицы не боясь что во время сортировки в таблице изменятся данные. Ровно это функция и делает. Если не видите смысла использовать эту функцию то просто не используйте и все. Никто не заставляет.
- Доктор, когда я вот так делаю, у меня болит. - А вы так не делайте.
Цитата
Если нужно заблокировать остальные потоки во время вывода таблицы, то при чем тут функция ssort?
Речь была не о выводе конкретно, а о любом доступе к "упорядоченной" таблице. Документация утверждает, что функция упорядочивает, но проверить и воспользоваться этим никак нельзя: если другие потоки не блокировать, то после завершения table.ssort() перед любой другой операцией упорядоченность может быть нарушена. Если же другие потоки блокировать, но зачем вообще "потокобезопасная сортировка" (не говоря о том, что блокировать в QLua API просто нечем)? Опять же, я прямо теряюсь в догадках, почему сей очевидный факт не находит у вас понимания.
Цитата
Цитата
kroki написал: Если GC запускаются в каждом потоке свой, то как они не "наступают друг другу на пятки", и не конфликтуют с выполнением другого потока?
В этом вопросе лучше и правильней обратиться к истокам. garbage collector не является нашей разработкой.
Цитата
kroki написал: Вопрос 6: как исключается ситуация коллизии между GC в разных потоках, запущенными одновременно? Не могут два разных GC одновременно начать удалять один и тот же объект?
В нашем ПО, это никак не исключается. Все работает так как это реализовали авторы Lua. Можете проверить поведение функции поставив ряд экспериментов.
Ага, моменты истины! Если ваши утверждения верны (а я надеюсь, что нет), то вся реализация QLua - абсолютно небезопасна относительно потоков. С ваших слов, вы взяли реализацию Lua, которая к многопоточности абсолютно не готова (там есть объект lua_thread, но это coroutines - совершенно другой зверь), и запустили в разных потоках два интерпретатора на одном environment (то есть используя один объект lua_State - ммм, значит GC все же один) - безо всякой синхронизации. Гляньте второй пост здесь - пишет как раз автор Lua, и говорит как раз то, что даже если environments разные, шарить один lua_State между потоками нельзя...
Ммм, последняя реплика Михаила содержит какой-то намек, но из поста автора Lua следует, что лочить только доступ к глобальным переменным - не достаточно, а если лочить все, что необходимо лочить, то это "makes all Lua code serialized and with a heavy lock/unlock overhead".
В общем я понял, что дело мутное. Но спасибо, Сергей!
L и T разделяют глобальные объекты, и доступ к этим объектам вы лочите. Дальше в основном потоке используете L, а в потоке main() используете T. Да, пишут, что такой вариант вполне рабочий. Ну и, видимо, как-то сериализуете запуск GC, чтобы не работал параллельно с другим потоком и не лез в его данные. Если не трудно, то все же скажите, как, и как именно "блокируются" потоки в table.sconcat() - это все хочется знать, чтобы понимать, как именно лучше писать Lua код, какая операция сколько стоит... Теперь, например, стало ясно, что в начале скрипта нужно писать
Код
local string, table, pairs, ipairs = string, table, pairs, ipairs
и т.д., чтобы не лочить global environment при каждом доступе к string.something() и т.д.
kroki, Из поста совершенно не понятно чего Вы от нас хотите. Вы делаете какие-то выводы, ради бога, это Ваше право. Вы спрашиваете зачем нужна функция, Вам был дан ответ и если не хотите ее использовать, Ваше право. В свое время мы сделали функции по запросу пользователей, для их конкретных задач. Ровно как сделали темную тему в терминале по просьбе пользователей, не хотите использовать темную тему никто не заставляет. Да LUA не наш продукт, мы только встроили уже готовое в терминал и мы не можем знать всех нюансов которые в него заложили его авторы, а только то что есть в открытых источниках, ровно также как и Вы. Если есть конкретные вопросы готовы ответить, пока их в Вашем посте не видно, а только какие-то рассуждения и умозаключения.
Если не трудно, то все же скажите, как, и как именно "блокируются" потоки в table.sconcat() - это все хочется знать, чтобы понимать, как именно лучше писать Lua код, какая операция сколько стоит...
в оригинальной версии table.concat, насколько я помню, блокировка происходит в момент получения элемента таблицы. В sconcat блокировка происходит на время выполнения всего цикла.
Цитата
Теперь, например, стало ясно, что в начале скрипта нужно писать
Код
local string, table, pairs, ipairs = string, table, pairs, ipairs
и т.д., чтобы не лочить global environment при каждом доступе к string.something() и т.д.
Это делается для ускорения обращения к нужным данным и не связано с блокировками.
Сегодня сделал несколько тестов и просмотрел код Lua, и наконец понял. Быть может будет полезно кому-то еще (ну и чтобы закрыть ветку):
0. В реализации Lua есть макросы lua_lock() и lua_unlock(), которые вызываются в коде Lua в нужных местах, но по умолчанию ничего не делают. QLua переопределяет эти макросы как вызовы EnterCriticalSection() и LeaveCriticalSection() - захват и освобождние рекурсивной блокировки (сам Lua рекурсивности не требует, но она нужна для реализации table.ssort()).
1. Перед запуском Lua программы реализация Lua делает вызов lua_lock(), то есть инструкции Lua программы выполняются при удерживаемой блокировке. Это значит, что хотя "потоки" - это настояшие потоки операционной системы, в каждый момент времени интерпретатор Lua работает только в одном потоке, параллельной работы интерпретаторов нет.
2. После некоторых инструкий Lua программы интерпретатор делает
Код
lua_unlock();
lua_lock();
то есть блокировка освобождается и тут же делается попытка захватить ее назад, но если другой поток ожидал блокировку, то он ее захватит первым и будет выполняться до тех пор, пока сам не сделает
Код
lua_unlock();
lua_lock();
(или не вызовет "Си" функцию - смотрим ниже) во время чего первый поток сможет захватить блокировку опять, и т.д.
3.Garbage collector всегда вызывается при удерживаемой блокировке.
4. Вызов "Си" функции из Lua программы (то есть функции, написанной не на Lua, а на каком-то другом языке) выполняется так:
Код
lua_unlock();
call external С function;
lua_lock();
То есть "Си" функции выполняются без удержания блокировки.
5. Все функции Lua C API (то есть те, которые могут вызываться из "Си" функций) - потокобезопасные, то есть делают lua_lock()/lua_unlock() где это необходимо.
6. Функции стандартной библиотеки Lua (такие, как table.sort) являются "Си" функциями, но не работают через Lua C API (дергают внутренний код напрямую) и не делают lua_lock()/lua_unlock() - то есть не являются потокобезопасными. Именно поэтому в QLua добавлены потокобезопасные аналоги некоторых функций, которые просто оборачивают своих младших братьев в lua_lock()/lua_unlock().
Теперь переводим документацию:
Цитата
Одновременная работа с таблицами из функций обратного вызова скрипта и функции main() может приводить к неопределенным ситуациям.
следует читать
Цитата
Одновременная работа с таблицами с использованием функций стандартной библиотеки Lua из функций обратного вызова скрипта и функции main() может приводить к неопределенным ситуациям.
То есть, функциями table.sort, unpack и т.п. работать с таблицей (потенциально) одновременно из разных потоков нельзя (нужно использовать потокобезопасные замены). Но вот просто код Lua выполнять можно, то есть можно писать
Код
shared_table[key] = val
global_var = nil
и т.п. - здесь функции библитотеки не задействованы, а инструкции программы Lua выполняются с удержанием блокировки. Также можно использовать "Си" функции сторонних библиотек - они работают через Lua C API, а, значит, потокобезопасны. Только не нужно путать потоковую безопасность и атомарность: если "Си" функция обещает вставить в какую-то таблицу 5 элементов, вполне возможно, что другой поток увидит, что пока вставлено только 3 элемента.
Цитата
Выполнение потокобезопасной функции блокирует выполнение кода в другом потоке до окончания работы функции.
следует читать
Цитата
Выполнение потокобезопасной функции блокирует выполнение Lua кода в другом потоке до окончания работы функции.
То есть, потокобезопасная функция удерживает блокировку и не дает ее другому интерпретатору, но если в другом потоке уже началось выполнение "Си" функции, то оно продолжится либо до завершения, либо до первого вызова Lua C API, и только тогда будет сделана попытка захватить блокировку.
Дальше сам себе отвечаю:
Цитата
Вопрос 1: приводит ли присвоение nil глобальным переменным из функции main() (то есть не главного потока) к неопределенным ситуациям?
Нет, присвоение делается кодом Lua, то есть под блокировкой.
Цитата
Вопрос 2: имеется ввиду, что другие потоки блокируются при попытке вызвать (ту же или какую-то другую) потокобезопасную функцию (классическая critical section)? Или же вообще все потоки волшебным образом останавливаются, ну, так, "на всякий случай"? :)
Блокируется интерпретатор Lua в другом потоке при попытке выполнть следующую инструкцию (либо "Си" функция при попытке вызвать Lua C API).
Цитата
Вопрос 3: как вообще тогда использовать table.ssort()?
По назначению - никак. Но можно использовать для создания "критических секций", то есть вызова произвольних функций при удерживаемой блокировке:
Код
table.ssort({ 0, 0 }, function()
-- код здесь выполняется под блокировкой
return true
end)
Только не забываем, что table.ssort() "виснет", если в функции сравнения возникает ошибка (проверял на Quik 7.14.1.7):
Код
function main()
table.ssort({ 1, "a" })
end
Поэтому в нужных местах используем pcall().
Цитата
Вопрос 4: в примере выше, что произойдет, если один поток изменит таблицу потокобезопасной функцией как раз в то время, когда другой поток находится где-то в дебрях интерпретатора Lua (внутри ipairs(), t[key], или еще где)? Что именно сделано, чтобы все не "взорвалось"? Или "так делать нельзя"?
ipairs() является функцией стандартной библиотеки Lua и не является потокобезопасной - "так делать нельзя". Lua код t[key] будет выполнен с удержанием той же блокировки, которая используется в потокобезопасной функции, так что выполнится или до, или после такой функции, но никак не одновременно - все будет безопасно.
Цитата
Вопрос 5: а какие "сообщения" можно посылать таким образом? Абсолютно любые "типы" Lua, или есть ограничения? Этот вопрос связан со следующим:
Абсолютно любые типы Lua.
Цитата
Вопрос 6: а как потоки дружат со сборщиком мусора (garbage collector)? Он один на все потоки, или по одному на каждый? Если один, то в каком потоке выполняется, и что именно сделано, чтобы GC в одном потоке не "взорвал" другой работающий поток, и не "взорвался" сам, считая свободной память, которую другой поток как раз начинает использовать)? Если же GC у каждого потока свой, то в случае message passing как в примере выше как именно передается ownership (то есть чей GC отвечает за рекламацию сообщения)?
GC может запуститься в любом потоке, но при удерживаемой блокировке. Поскольку настоящей параллельной работы интерпретаторов Lua не бывает, то GC работает точно так же, как и в случае обычных сопроцедур Lua (coroutines) в одном потоке: никаких раздельных ownership нет, GC проводит рекламацию объектов в environments обоих потоков (а второй интерпретатор ждет освобождения блокировки) - все безопасно.
Стал программировать, руководствуясь своей "докой" выше, и уперся в противоречие: с одной стороны утверждаю, что GC всегда работает при удерживаемой блокировке (что правда), с другой стороны collectgarbage() - функция стандартной библиотеки Lua, и я написал, что таковые работают не через Lua C API, и потому не потокобезопасны.
Так вот последнее было не верно: пункт 6 меняем на:
6. Функции стандартной библиотеки Lua (такие, как table.sort) являются "Си" функциями.
Точка. То есть как и остальные "Си" функции они работают через Lua C API, который делает lua_lock()/lua_unlock() в нужных местах. Но их работа не атомарна. Например, реализация table.concat() делает (на Си)
Код
static int tconcat (lua_State *L) {
luaL_Buffer b;
size_t lsep;
int i, last;
const char *sep = luaL_optlstring(L, 2, "", &lsep);
luaL_checktype(L, 1, LUA_TTABLE);
i = luaL_optint(L, 3, 1);
A: last = luaL_opt(L, luaL_checkint, 4, luaL_getn(L, 1));
luaL_buffinit(L, &b);
for (; i <= last; i++) {
lua_rawgeti(L, 1, i);
B: luaL_argcheck(L, lua_isstring(L, -1), 1, "table contains non-strings");
luaL_addvalue(&b);
if (i != last)
luaL_addlstring(&b, sep, lsep);
}
luaL_pushresult(&b);
return 1;
}
то есть в строке A запоминает количество элементов, а в строке B проверяется, что очередной элемент - строка. Но блокировка захватывается только на время вызова функций Lua C API (например, lua_rawgeti() в коде выше - Михаил как раз об этом и писал), а во время собственно работы цикла не удерживается. Это значит, что другой поток может удалить какие-то элементы из массива, сделав его короче, но код все еще помнит старый размер, и тогда на очередной итерации цикл вытащит nil, и проверка типа "строка" бросит исключение "table contains non-strings".
То есть обертка table.sconcat() нужна для обеспечения атомарности работы (потокобезопасность в строгом смысле уже была и в оригинальной функции).
Вообще производительность Lua при заданных макросах lua_lock()/lua_unlock() просто ужасная, почти на каждом шагу освобождение и захват блокировки. Поэтому у себя обернул все критические callbacks в table.ssort(), чтобы не было постоянной борьбы за блокировку с main(). Кстати, в некоторых старых версиях Quik (например, 7.5.0.72) все callbacks вызывались при удержанной блокирокве, но в 7.14.1.7 это уже не так.
Ниже тест "многопоточной" скорости выполнения. Если запустить скрипт и дождаться окончания работы, но борьбы за блокировку нет. Если же запустить скрипт и тут же нажать кнопку "Остановить", то будет борьба, и время выпонения цикла в main() существенно возрастет:
Код
local total = 100000000
function OnStop()
local var
for i = 1, total do
var = i
end
return 15000
end
function main()
local start = os.clock()
local var
for i = 1, total do
var = i
end
local elapsed = os.clock() - start
message("CPU time: "..elapsed)
end
kroki написал: Вообще производительность Lua при заданных макросах lua_lock() / lua_unlock() просто ужасная, почти на каждом шагу освобождение и захват блокировки.
kroki написал: Поэтому у себя обернул все критические callbacks в table.ssort() , чтобы не было постоянной борьбы за блокировку с main().
Цитата
kroki написал: Но можно использовать для создания "критических секций", то есть вызова произвольних функций при удерживаемой блокировке:
Код
table.ssort ({ 0 , 0 }, function ()
-- код здесь выполняется под блокировкой
return true
end )
Интересная идея. Возьму на вооружение. Код, действительно, внутри ssort выполняется быстрее. Но все же в два раза дольше, чем в чистом Lua.
Цитата
kroki написал: хотя "потоки" - это настояшие потоки операционной системы, в каждый момент времени интерпретатор Lua работает только в одном потоке, параллельной работы интерпретаторов нет.
Старатель написал: Вопрос разработчикам: верно ли, что в QUIK, как таковой, многопоточности нет?
В QUIK есть многопоточность
Цитата
Старатель написал: Да, есть два потока: основной и main. Но в каждый момент времени работают команды только из одного потока. Просто происходит переключение между потоками на уровне ОС.
так обрабатывается ситуация при одновременном доступе к одному ресурсу
Надо делать так, как надо. А как не надо - делать не надо.
Старатель написал: Вопрос разработчикам: верно ли, что в QUIK, как таковой, многопоточности нет?
В QUIK есть многопоточность
Цитата
Старатель написал: Да, есть два потока: основной и main. Но в каждый момент времени работают команды только из одного потока. Просто происходит переключение между потоками на уровне ОС.
так обрабатывается ситуация при одновременном доступе к одному ресурсу
Как раз собственно интерпретатор Lua и является таким ресурсом, которым не получатеся воспользоваться из нескольких потоков одновременно. Возможно не все разработчики об этом знают, смотрите в код Lua - так надежнее...