Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 30 additions & 14 deletions src/OneScript.Core/Compilation/CompilerFrontendBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

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);
Expand Down
97 changes: 56 additions & 41 deletions src/ScriptEngine/RuntimeEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)
Expand Down Expand Up @@ -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;
Expand Down
87 changes: 87 additions & 0 deletions src/Tests/OneScript.Core.Tests/GlobalSymbolsThreadSafetyTests.cs
Original file line number Diff line number Diff line change
@@ -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<GlobalSymbolsTestContext>
{
}

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<string>();
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();
}
}
}
65 changes: 63 additions & 2 deletions tests/librarytest.os
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
#Использовать "customlib"

Перем юТест;
Перем ГотовыеЗадания;

Функция ПолучитьСписокТестов(Знач Тесты) Экспорт
юТест = Тесты;

Список = Новый Массив;
Список.Добавить("ТестДолжен_ИспользоватьВстроенныйЗагрузчик");
Список.Добавить("ТестДолжен_ИспользоватьПользовательскийЗагрузчик");

Список.Добавить("ТестДолжен_ЗагрузитьБиблиотекуИзНесколькихЗаданийОдновременно");

Возврат Список;

КонецФункции
Expand All @@ -33,5 +35,64 @@

НекийОбщийМодуль.УстановитьПоСсылке(Супер.А);
юТест.ПроверитьРавенство(100, Супер.А);


КонецПроцедуры

Процедура ТестДолжен_ЗагрузитьБиблиотекуИзНесколькихЗаданийОдновременно() Экспорт

Путь = ОбъединитьПути(ТекущийСценарий().Каталог, "slowlib");
ГотовыеЗадания = Новый Массив(4);
Для Индекс = 0 По ГотовыеЗадания.ВГраница() Цикл
ГотовыеЗадания[Индекс] = Ложь;
КонецЦикла;

Задания = Новый Массив;
Для Индекс = 0 По ГотовыеЗадания.ВГраница() Цикл
Параметры = Новый Массив;
Параметры.Добавить(Путь);
Параметры.Добавить(Индекс);
Задания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ИспользоватьМедленнуюБиблиотеку", Параметры));
КонецЦикла;
ФоновыеЗадания.ОжидатьВсе(Задания);

Для Каждого Задание Из Задания Цикл
Если Задание.ИнформацияОбОшибке <> Неопределено Тогда
ВызватьИсключение Задание.ИнформацияОбОшибке.ПодробноеОписаниеОшибки();
КонецЕсли;
юТест.ПроверитьРавенство("Привет", Задание.Результат);
КонецЦикла;

КонецПроцедуры

Функция ИспользоватьМедленнуюБиблиотеку(Путь, Индекс) Экспорт

// Все задания начинают подключать библиотеку одновременно
ГотовыеЗадания[Индекс] = Истина;
Для Номер = 1 По 500 Цикл
Если ВсеЗаданияГотовы() Тогда
Прервать;
КонецЕсли;
Приостановить(10);
КонецЦикла;

Сценарий = ЗагрузитьСценарийИзСтроки(
"#Использовать """ + Путь + """
|Функция Привет() Экспорт
| Возврат МедленныйМодуль.Привет();
|КонецФункции");
Возврат Сценарий.Привет();

КонецФункции

Функция ВсеЗаданияГотовы()

// По индексу, а не Для Каждого: другие задания в это время пишут в массив
Для Индекс = 0 По ГотовыеЗадания.ВГраница() Цикл
Если Не ГотовыеЗадания[Индекс] Тогда
Возврат Ложь;
КонецЕсли;
КонецЦикла;

Возврат Истина;

КонецФункции
3 changes: 3 additions & 0 deletions tests/slowlib/module.os
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Функция Привет() Экспорт
Возврат "Привет";
КонецФункции
8 changes: 8 additions & 0 deletions tests/slowlib/package-loader.os
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Процедура ПриЗагрузкеБиблиотеки(Знач Путь, СтандартнаяОбработка, Отказ)

// Загрузка долгая: пока она идет, библиотеку запрашивают из других потоков
Приостановить(300);
СтандартнаяОбработка = Ложь;
ДобавитьМодуль(ОбъединитьПути(Путь, "module.os"), "МедленныйМодуль");

КонецПроцедуры
Loading