Потоковая безопасность в QLua - документация и реальность

Страницы: 1
RSS
Потоковая безопасность в QLua - документация и реальность
 
Ниже несколько вопросов к поддержке и/или знатокам по теме потоковой безопасности.

Читаем документацию:
Цитата
Одновременная работа с таблицами из функций обратного вызова скрипта и функции 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 к ним не относится.
 
Цитата
Sergey Gorokhov написал:
Цитата
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 написал:
Попробую угадать:
Не надо искать скрытый смысл там где его просто нет.
Фраза "блокирует выполнение кода в другом потоке" означает ровно то что означает без скрытого смысла.
И тут нет никакого сокрального знания.

Цитата
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  отвечает за рекламацию сообщения)?
Сборщик мусора один. Вообще потоки не являются потоками в "классическом" понимании. Это скорее новый объект с отдельным стеком для вызовов функций и локальных переменных. При этом у них общие глобальные данные. Попытка обращения к ним из одного "потока" блокирует другой, если он тоже обращается к ним.
 
Цитата
Sergey Gorokhov написал:
Цитата
kroki   написал:
По-моему в ваших словах противоречие:
По моему это Ваш код противоречит Вашим словам.
Еще раз, согласно Вашему коду никакого одновременного присвоения нет. А значит вопрос совершенно никак не связан с цитатой из документации.
В документации не идет речи о присвоении, есть выражение "одновременная работа".  То есть не только запись, но и чтение.  Потоки не могут не читать global environment: всякий раз, когда вы пишите table.something() или string.something(), вы читаете значения _G["table"] и _G["string"].  Давайте еще раз:
  1. Является ли присвоение переменной значения nil операцией изменения таблицы, в которой переменная хранится? - Да, в Lua присвоение _G["var"] = nil это операция удаления из таблицы.
  2. Обращаются ли разные потоки к таблице глобальных переменных? - Да, как сказано выше, этого не избежать.
  3. Можно ли одновременно менять таблицу из одного потока и читать из другого без блокировок? - Нет, согласно документации (и здравому смыслу) нужно использовать потокобезопасные функции.
  4. Используются ли потокобезопасные функции при обращении к глобальным переменным? - Нет, к глобальным переменным обращаются напрямую.
Отсюда вывод: присваивать глобальным переменным значение 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".

В общем я понял, что дело мутное.  Но спасибо, Сергей!
 
Все, кажется я смог перевести слова Михаила: в основном потоке вы вызываете
Код
lua_State *L = lua_newstate(...);
lua_State *T = lua_newthread(L);

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 работает только в одном потоке, параллельной работы интерпретаторов нет.

У разработчиков на этот счет другое мнение:

Цитата
Sergey Gorokhov написал:
Цитата
Старатель написал:
Вопрос разработчикам: верно ли, что в QUIK, как таковой, многопоточности нет?
В QUIK есть многопоточность
Цитата
Старатель написал:
Да, есть два потока: основной и main. Но в каждый момент времени работают команды только из одного потока. Просто происходит переключение между потоками на уровне ОС.
так обрабатывается ситуация при одновременном доступе к одному ресурсу
Надо делать так, как надо. А как не надо - делать не надо.
 
Интересно, что в обработчике OnInit или BODY код, помещенный в ssort, также работает быстрее, хотя main на этом этапе ещё не включается в работу.
Надо делать так, как надо. А как не надо - делать не надо.
 
У разработчиков на этот счет  другое мнение :

Цитата
Sergey Gorokhov написал:
 
Цитата
Старатель  написал:
Вопрос разработчикам: верно ли, что в QUIK, как таковой, многопоточности нет?
В QUIK есть многопоточность
 
Цитата
Старатель  написал:
Да, есть два потока: основной и main. Но в каждый момент времени работают команды только из одного потока. Просто происходит переключение между потоками на уровне ОС.
так обрабатывается ситуация при одновременном доступе к одному ресурсу
Как раз собственно интерпретатор Lua и является таким ресурсом, которым не получатеся воспользоваться из нескольких потоков одновременно.  Возможно не все разработчики об этом знают, смотрите в код Lua - так надежнее...
Страницы: 1
Читают тему
Наверх