diff --git a/src/OneScript.Core/Compilation/CompilerFrontendBase.cs b/src/OneScript.Core/Compilation/CompilerFrontendBase.cs index a3272ebe0..a99d2df3f 100644 --- a/src/OneScript.Core/Compilation/CompilerFrontendBase.cs +++ b/src/OneScript.Core/Compilation/CompilerFrontendBase.cs @@ -55,34 +55,50 @@ public SymbolScope FillSymbols(Type targetType) private SymbolScope ModuleSymbols { get; set; } + // Таблица окружения одна на все потоки, а #Использовать дописывает в нее модули библиотек + // посреди компиляции. Поэтому компиляции по общей таблице идут по одной, под блокировкой + // на самой таблице, и окружение меняет ее под этой же блокировкой. + private object CompilationLock => (object)SharedSymbols ?? _ownCompilationLock; + + private readonly object _ownCompilationLock = new object(); + public IExecutableModule Compile(SourceCode source, IBslProcess process, Type classType = null) { - var lexer = CreatePreprocessor(source); - var symbols = PrepareSymbols(); - var parsedModule = ParseSyntaxConstruction(lexer, source, p => p.ParseStatefulModule()); + lock (CompilationLock) + { + var lexer = CreatePreprocessor(source); + var symbols = PrepareSymbols(); + var parsedModule = ParseSyntaxConstruction(lexer, source, p => p.ParseStatefulModule()); - return CompileInternal(symbols, parsedModule, classType, process); + return CompileInternal(symbols, parsedModule, classType, process); + } } public IExecutableModule CompileExpression(SourceCode source) { - var lexer = new DefaultLexer + lock (CompilationLock) { - Iterator = source.CreateIterator() - }; - var symbols = PrepareSymbols(); - var parsedModule = ParseSyntaxConstruction(lexer, source, p => p.ParseExpression()); + var lexer = new DefaultLexer + { + Iterator = source.CreateIterator() + }; + var symbols = PrepareSymbols(); + var parsedModule = ParseSyntaxConstruction(lexer, source, p => p.ParseExpression()); - return CompileExpressionInternal(symbols, parsedModule); + return CompileExpressionInternal(symbols, parsedModule); + } } public IExecutableModule CompileBatch(SourceCode source) { - var lexer = CreatePreprocessor(source); - var symbols = PrepareSymbols(); - var parsedModule = ParseSyntaxConstruction(lexer, source, p => p.ParseStatefulModule()); + lock (CompilationLock) + { + var lexer = CreatePreprocessor(source); + var symbols = PrepareSymbols(); + var parsedModule = ParseSyntaxConstruction(lexer, source, p => p.ParseStatefulModule()); - return CompileBatchInternal(symbols, parsedModule); + return CompileBatchInternal(symbols, parsedModule); + } } protected abstract IExecutableModule CompileInternal(SymbolTable symbols, ModuleNode parsedModule, Type classType, IBslProcess process); diff --git a/src/ScriptEngine/RuntimeEnvironment.cs b/src/ScriptEngine/RuntimeEnvironment.cs index bb450db38..1eda3792e 100644 --- a/src/ScriptEngine/RuntimeEnvironment.cs +++ b/src/ScriptEngine/RuntimeEnvironment.cs @@ -20,6 +20,8 @@ namespace ScriptEngine [Obsolete("Use interface IRuntimeEnvironment")] public class RuntimeEnvironment : IRuntimeEnvironment, ILibraryManager { + // Компиляторы всех потоков читают эту таблицу, а #Использовать и ПодключитьВнешнююКомпоненту дописывают в нее. + // Компиляция по ней и любые ее изменения идут под блокировкой на самой таблице (см. CompilerFrontendBase) private readonly SymbolTable _symbols = new SymbolTable(); private SymbolScope _scopeOfGlobalProperties; @@ -35,16 +37,14 @@ public RuntimeEnvironment() _libraryManager = new LibraryManager(_injectedProperties); } + // Вызывается под блокировкой _symbols private void CreateGlobalScopeIfNeeded() { if (_scopeOfGlobalProperties != null) return; - - lock (_injectedProperties) - { - _scopeOfGlobalProperties ??= _symbols.PushContext(_injectedProperties); - _contexts.Add(_injectedProperties); - } + + _scopeOfGlobalProperties = _symbols.PushContext(_injectedProperties); + _contexts.Add(_injectedProperties); } public void InjectObject(IAttachableContext context) @@ -84,65 +84,80 @@ private void InjectPropertyInternal( { throw new ArgumentException("Invalid identifier", nameof(alias)); } - CreateGlobalScopeIfNeeded(); - var num = _injectedProperties.Insert(value, identifier, true, !readOnly); - - var bslPropertyInfo = _injectedProperties.GetPropertyInfo(num); - IVariableSymbol registeredSymbol; - if (ownerPackage == null) + lock (_symbols) { - registeredSymbol = new WrappedPropertySymbol(bslPropertyInfo) + CreateGlobalScopeIfNeeded(); + var num = _injectedProperties.Insert(value, identifier, true, !readOnly); + + var bslPropertyInfo = _injectedProperties.GetPropertyInfo(num); + IVariableSymbol registeredSymbol; + if (ownerPackage == null) { - Name = identifier, - Alias = alias - }; - } - else - { - registeredSymbol = new WrappedLibraryPropertySymbol(bslPropertyInfo, ownerPackage) + registeredSymbol = new WrappedPropertySymbol(bslPropertyInfo) + { + Name = identifier, + Alias = alias + }; + } + else { - Name = identifier, - Alias = alias - }; + registeredSymbol = new WrappedLibraryPropertySymbol(bslPropertyInfo, ownerPackage) + { + Name = identifier, + Alias = alias + }; + } + + _scopeOfGlobalProperties.DefineVariable(registeredSymbol); } - - _scopeOfGlobalProperties.DefineVariable(registeredSymbol); } public void InjectGlobalProperty(IValue value, BslPropertyInfo definition) { - CreateGlobalScopeIfNeeded(); - _injectedProperties.Insert(value, definition); - - var symbol = new WrappedPropertySymbol(definition) + lock (_symbols) { - Name = definition.Name, - Alias = definition.Alias - }; + CreateGlobalScopeIfNeeded(); + _injectedProperties.Insert(value, definition); - _scopeOfGlobalProperties.DefineVariable(symbol); + var symbol = new WrappedPropertySymbol(definition) + { + Name = definition.Name, + Alias = definition.Alias + }; + + _scopeOfGlobalProperties.DefineVariable(symbol); + } } private void RegisterObject(IAttachableContext context) { - _symbols.PushContext(context); - _contexts.Add(context); + lock (_symbols) + { + _symbols.PushContext(context); + _contexts.Add(context); + } } public void SetGlobalProperty(string propertyName, IValue value) { - _symbols.FindVariable(propertyName, out var binding); + lock (_symbols) + { + _symbols.FindVariable(propertyName, out var binding); - var context = _contexts[binding.ScopeNumber]; - context.SetPropValue(binding.MemberNumber, value); + var context = _contexts[binding.ScopeNumber]; + context.SetPropValue(binding.MemberNumber, value); + } } public IValue GetGlobalProperty(string propertyName) { - _symbols.FindVariable(propertyName, out var binding); + lock (_symbols) + { + _symbols.FindVariable(propertyName, out var binding); - var context = _contexts[binding.ScopeNumber]; - return context.GetPropValue(binding.MemberNumber); + var context = _contexts[binding.ScopeNumber]; + return context.GetPropValue(binding.MemberNumber); + } } public SymbolTable GetSymbolTable() => _symbols; diff --git a/src/Tests/OneScript.Core.Tests/GlobalSymbolsThreadSafetyTests.cs b/src/Tests/OneScript.Core.Tests/GlobalSymbolsThreadSafetyTests.cs new file mode 100644 index 000000000..9b1c812b0 --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/GlobalSymbolsThreadSafetyTests.cs @@ -0,0 +1,87 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading; +using FluentAssertions; +using OneScript.Contexts; +using ScriptEngine.Hosting; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; +using Xunit; + +namespace OneScript.Core.Tests +{ + [GlobalContext(ManualRegistration = true)] + public class GlobalSymbolsTestContext : GlobalContextBase + { + } + + public class GlobalSymbolsThreadSafetyTests + { + [Fact] + public void ScriptsCompileWhileGlobalSymbolsAreAdded() + { + var engine = DefaultEngineBuilder.Create().SetDefaultOptions().Build(); + engine.Initialize(); + engine.Environment.InjectGlobalProperty(ValueFactory.Create("есть"), "ИзвестноеСвойство", true); + + // Много обращений к глобальному свойству: каждое ищет имя в общей области глобальных свойств + var code = string.Concat(Enumerable.Repeat("А = ИзвестноеСвойство;\n", 200)); + var errors = new ConcurrentQueue(); + var stop = false; + var compiled = 0; + const int compilersCount = 4; + var compilers = Enumerable.Range(0, compilersCount).Select(_ => new Thread(() => + { + while (!Volatile.Read(ref stop)) + { + try + { + var source = engine.Loader.FromString(code); + engine.GetCompilerService().Compile(source, engine.NewProcess()); + } + catch (Exception e) + { + errors.Enqueue($"{e.GetType().Name}: {e.Message}"); + } + Interlocked.Increment(ref compiled); + } + }) { IsBackground = true }).ToArray(); + + foreach (var thread in compilers) + { + thread.Start(); + } + + // Компиляторы уже работают, когда начинается регистрация + SpinWait.SpinUntil(() => Volatile.Read(ref compiled) >= compilersCount, 10000).Should().BeTrue(); + var compiledBeforeRegistration = Volatile.Read(ref compiled); + + // Как загрузка библиотеки и ПодключитьВнешнююКомпоненту в другом потоке + for (var i = 0; i < 30000; i++) + { + engine.Environment.InjectGlobalProperty(ValueFactory.Create(i), "Свойство" + i, true); + if (i % 1000 == 0) + engine.Environment.InjectObject(new GlobalSymbolsTestContext()); + } + + var compiledDuringRegistration = Volatile.Read(ref compiled) - compiledBeforeRegistration; + + Volatile.Write(ref stop, true); + foreach (var thread in compilers) + { + thread.Join(); + } + + compiledDuringRegistration.Should().BePositive("компиляции должны идти вперемежку с регистрацией"); + errors.Should().BeEmpty(); + } + } +} diff --git a/tests/librarytest.os b/tests/librarytest.os index 2bc0f56cc..62bd179f3 100644 --- a/tests/librarytest.os +++ b/tests/librarytest.os @@ -2,6 +2,7 @@ #Использовать "customlib" Перем юТест; +Перем ГотовыеЗадания; Функция ПолучитьСписокТестов(Знач Тесты) Экспорт юТест = Тесты; @@ -9,7 +10,8 @@ Список = Новый Массив; Список.Добавить("ТестДолжен_ИспользоватьВстроенныйЗагрузчик"); Список.Добавить("ТестДолжен_ИспользоватьПользовательскийЗагрузчик"); - + Список.Добавить("ТестДолжен_ЗагрузитьБиблиотекуИзНесколькихЗаданийОдновременно"); + Возврат Список; КонецФункции @@ -33,5 +35,64 @@ НекийОбщийМодуль.УстановитьПоСсылке(Супер.А); юТест.ПроверитьРавенство(100, Супер.А); - + +КонецПроцедуры + +Процедура ТестДолжен_ЗагрузитьБиблиотекуИзНесколькихЗаданийОдновременно() Экспорт + + Путь = ОбъединитьПути(ТекущийСценарий().Каталог, "slowlib"); + ГотовыеЗадания = Новый Массив(4); + Для Индекс = 0 По ГотовыеЗадания.ВГраница() Цикл + ГотовыеЗадания[Индекс] = Ложь; + КонецЦикла; + + Задания = Новый Массив; + Для Индекс = 0 По ГотовыеЗадания.ВГраница() Цикл + Параметры = Новый Массив; + Параметры.Добавить(Путь); + Параметры.Добавить(Индекс); + Задания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ИспользоватьМедленнуюБиблиотеку", Параметры)); + КонецЦикла; + ФоновыеЗадания.ОжидатьВсе(Задания); + + Для Каждого Задание Из Задания Цикл + Если Задание.ИнформацияОбОшибке <> Неопределено Тогда + ВызватьИсключение Задание.ИнформацияОбОшибке.ПодробноеОписаниеОшибки(); + КонецЕсли; + юТест.ПроверитьРавенство("Привет", Задание.Результат); + КонецЦикла; + КонецПроцедуры + +Функция ИспользоватьМедленнуюБиблиотеку(Путь, Индекс) Экспорт + + // Все задания начинают подключать библиотеку одновременно + ГотовыеЗадания[Индекс] = Истина; + Для Номер = 1 По 500 Цикл + Если ВсеЗаданияГотовы() Тогда + Прервать; + КонецЕсли; + Приостановить(10); + КонецЦикла; + + Сценарий = ЗагрузитьСценарийИзСтроки( + "#Использовать """ + Путь + """ + |Функция Привет() Экспорт + | Возврат МедленныйМодуль.Привет(); + |КонецФункции"); + Возврат Сценарий.Привет(); + +КонецФункции + +Функция ВсеЗаданияГотовы() + + // По индексу, а не Для Каждого: другие задания в это время пишут в массив + Для Индекс = 0 По ГотовыеЗадания.ВГраница() Цикл + Если Не ГотовыеЗадания[Индекс] Тогда + Возврат Ложь; + КонецЕсли; + КонецЦикла; + + Возврат Истина; + +КонецФункции diff --git a/tests/slowlib/module.os b/tests/slowlib/module.os new file mode 100644 index 000000000..4dc4885ab --- /dev/null +++ b/tests/slowlib/module.os @@ -0,0 +1,3 @@ +Функция Привет() Экспорт + Возврат "Привет"; +КонецФункции diff --git a/tests/slowlib/package-loader.os b/tests/slowlib/package-loader.os new file mode 100644 index 000000000..02c3fcdc1 --- /dev/null +++ b/tests/slowlib/package-loader.os @@ -0,0 +1,8 @@ +Процедура ПриЗагрузкеБиблиотеки(Знач Путь, СтандартнаяОбработка, Отказ) + + // Загрузка долгая: пока она идет, библиотеку запрашивают из других потоков + Приостановить(300); + СтандартнаяОбработка = Ложь; + ДобавитьМодуль(ОбъединитьПути(Путь, "module.os"), "МедленныйМодуль"); + +КонецПроцедуры