From 4428694ba83b16ed0b760623dd020084d9cf039f Mon Sep 17 00:00:00 2001 From: sfaqer Date: Wed, 30 Sep 2026 20:59:50 +1000 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=B5=D0=BB=D0=BA=D0=B8=D0=B5=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=82=D0=BE=D0=BA=D0=BE=D0=B1=D0=B5=D0=B7=D0=BE?= =?UTF-8?q?=D0=BF=D0=B0=D1=81=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Общие коллекции, которые меняются во время работы, читались без синхронизации: предупреждения об устаревших методах, глобальные экземпляры, макеты, точки останова отладчика. Коллекции заменены на конкурентные, точки останова подменяются целиком, вывод с цветом в Сообщить идет под блокировкой. В мапперах методов и свойств список, который публикуется двойной проверкой, стал volatile. Co-Authored-By: Claude Opus 5.5 --- .../DefaultBreakpointManager.cs | 45 +++++--- .../TemplateStorage.cs | 20 ++-- .../Machine/Contexts/AutoContext.cs | 6 +- .../Machine/Contexts/ContextMethodMapper.cs | 3 +- .../Machine/Contexts/ContextPropertyMapper.cs | 3 +- .../Machine/GlobalInstancesManager.cs | 10 +- .../TemplateStorageTests.cs | 72 +++++++++++++ .../BreakpointManagerTests.cs | 101 ++++++++++++++++++ src/oscript/ConsoleHostImpl.cs | 12 +++ 9 files changed, 240 insertions(+), 32 deletions(-) create mode 100644 src/Tests/OneScript.Core.Tests/TemplateStorageTests.cs create mode 100644 src/Tests/OneScript.DebugProtocol.Test/BreakpointManagerTests.cs diff --git a/src/OneScript.DebugServices/DefaultBreakpointManager.cs b/src/OneScript.DebugServices/DefaultBreakpointManager.cs index e9661a655..8928bb583 100644 --- a/src/OneScript.DebugServices/DefaultBreakpointManager.cs +++ b/src/OneScript.DebugServices/DefaultBreakpointManager.cs @@ -5,6 +5,7 @@ This Source Code Form is subject to the terms of the at http://mozilla.org/MPL/2.0/. ----------------------------------------------------------*/ +using System; using System.Collections.Generic; using System.Linq; using OneScript.Commons; @@ -14,37 +15,49 @@ namespace OneScript.DebugServices { public class DefaultBreakpointManager : IBreakpointManager { - private readonly Dictionary _exceptionBreakpointsFilters = new Dictionary(); - private readonly List _breakpoints = new List(); + // Точки задает поток отладчика, а проверяют потоки скриптов: списки не меняются, а подменяются целиком + private volatile Dictionary _exceptionBreakpointsFilters = new Dictionary(); + private volatile BreakpointDescriptor[] _breakpoints = Array.Empty(); + private readonly object _lock = new object(); private int _idsGenerator; public void SetExceptionBreakpoints((string Id, string Condition)[] filters) { - _exceptionBreakpointsFilters?.Clear(); - filters?.ForEach(c =>_exceptionBreakpointsFilters.Add(c.Id, c.Condition)); + var newFilters = new Dictionary(); + filters?.ForEach(c => newFilters.Add(c.Id, c.Condition)); + _exceptionBreakpointsFilters = newFilters; } public void SetBreakpoints(string module, (int Line, string Condition)[] breakpoints) { - var cleaned = _breakpoints.Where(x => x.Module != module) - .ToList(); + lock (_lock) + { + var cleaned = _breakpoints.Where(x => x.Module != module) + .ToList(); - var range = breakpoints.Select(x => new BreakpointDescriptor(_idsGenerator++) { LineNumber = x.Line, Module = module, Condition = x.Condition }); - cleaned.AddRange(range); - _breakpoints.Clear(); - _breakpoints.AddRange(cleaned); + var range = breakpoints.Select(x => new BreakpointDescriptor(_idsGenerator++) { LineNumber = x.Line, Module = module, Condition = x.Condition }); + cleaned.AddRange(range); + _breakpoints = cleaned.ToArray(); + } } public bool FindBreakpoint(string module, int line) - => _breakpoints.Find(x => x.Module.Equals(module) && x.LineNumber == line) != null; + => Find(module, line) != null; + // Точку могли снять между FindBreakpoint и GetCondition public string GetCondition(string module, int line) - => _breakpoints.Find(x => x.Module.Equals(module) && x.LineNumber == line).Condition; + => Find(module, line)?.Condition; + + private BreakpointDescriptor Find(string module, int line) + => Array.Find(_breakpoints, x => x.Module.Equals(module) && x.LineNumber == line); public void Clear() { - _breakpoints.Clear(); - _exceptionBreakpointsFilters.Clear(); + lock (_lock) + { + _breakpoints = Array.Empty(); + } + _exceptionBreakpointsFilters = new Dictionary(); } public bool StopOnAnyException(string message) @@ -55,7 +68,7 @@ public bool StopOnUncaughtException(string message) private bool NeedStopOnException(string filterId, string message) { - if (_exceptionBreakpointsFilters?.TryGetValue(filterId, out var condition) == true) + if (_exceptionBreakpointsFilters.TryGetValue(filterId, out var condition)) { if (string.IsNullOrEmpty(condition)) return true; @@ -66,4 +79,4 @@ private bool NeedStopOnException(string filterId, string message) return false; } } -} \ No newline at end of file +} diff --git a/src/ScriptEngine.HostedScript/TemplateStorage.cs b/src/ScriptEngine.HostedScript/TemplateStorage.cs index ea5b61859..f32a634d1 100644 --- a/src/ScriptEngine.HostedScript/TemplateStorage.cs +++ b/src/ScriptEngine.HostedScript/TemplateStorage.cs @@ -6,6 +6,7 @@ This Source Code Form is subject to the terms of the ----------------------------------------------------------*/ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using OneScript.Contexts; using OneScript.Contexts.Enums; @@ -22,28 +23,31 @@ namespace ScriptEngine.HostedScript public class TemplateStorage : GlobalContextBase, IDisposable { private readonly ITemplateFactory _factory; - private readonly Dictionary _templates = new Dictionary(); + // Макеты регистрируют библиотеки, которые могут загружаться во время работы, пока их читают другие потоки + private readonly ConcurrentDictionary _templates = new ConcurrentDictionary(); public TemplateStorage(ITemplateFactory factory) { _factory = factory; } - + public void RegisterTemplate(string file, string name, TemplateKind kind) { if (_templates.ContainsKey(name)) throw RuntimeException.InvalidArgumentValue(name); - + var template = _factory.CreateTemplate(file, kind); - _templates.Add(name, template); + if (!_templates.TryAdd(name, template)) + { + template.Dispose(); + throw RuntimeException.InvalidArgumentValue(name); + } } - + public void RegisterTemplate(string name, ITemplate template) { - if (_templates.ContainsKey(name)) + if (!_templates.TryAdd(name, template)) throw RuntimeException.InvalidArgumentValue(name); - - _templates.Add(name, template); } diff --git a/src/ScriptEngine/Machine/Contexts/AutoContext.cs b/src/ScriptEngine/Machine/Contexts/AutoContext.cs index 36933577c..bc1e9ddc7 100644 --- a/src/ScriptEngine/Machine/Contexts/AutoContext.cs +++ b/src/ScriptEngine/Machine/Contexts/AutoContext.cs @@ -5,6 +5,7 @@ This Source Code Form is subject to the terms of the at http://mozilla.org/MPL/2.0/. ----------------------------------------------------------*/ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using OneScript.Contexts; @@ -19,7 +20,7 @@ public abstract class AutoContext : PropertyNameIndexAccessor where T { private static readonly ContextPropertyMapper _properties = new ContextPropertyMapper(); private static readonly ContextMethodsMapper _methods = new ContextMethodsMapper(); - private static readonly HashSet _warnedDeprecatedMethods = new HashSet(); + private static readonly ConcurrentDictionary _warnedDeprecatedMethods = new ConcurrentDictionary(); private static readonly TypeDescriptor _objectType = typeof(TInstance).GetTypeFromClassMarkup(); protected AutoContext() : base(_objectType) @@ -118,12 +119,11 @@ private void CheckIfCallIsPossible(int methodNumber, IValue[] arguments) { throw RuntimeException.DeprecatedMethodCall(methodInfo.Name); } - if (_warnedDeprecatedMethods.Contains(methodNumber)) + if (!_warnedDeprecatedMethods.TryAdd(methodNumber, 0)) { return; } SystemLogger.Write($"ВНИМАНИЕ! Вызов устаревшего метода {methodInfo.Name}"); - _warnedDeprecatedMethods.Add(methodNumber); } public override void CallAsProcedure(int methodNumber, IValue[] arguments, IBslProcess process) diff --git a/src/ScriptEngine/Machine/Contexts/ContextMethodMapper.cs b/src/ScriptEngine/Machine/Contexts/ContextMethodMapper.cs index cce6144f0..2054df6af 100644 --- a/src/ScriptEngine/Machine/Contexts/ContextMethodMapper.cs +++ b/src/ScriptEngine/Machine/Contexts/ContextMethodMapper.cs @@ -21,7 +21,8 @@ namespace ScriptEngine.Machine.Contexts public class ContextMethodsMapper { - private List _methodPtrs; + // Публикуется последним: кто увидел заполненный список, увидит и словарь имен + private volatile List _methodPtrs; private IdentifiersTrie _methodNumbers; private readonly object _locker = new object(); diff --git a/src/ScriptEngine/Machine/Contexts/ContextPropertyMapper.cs b/src/ScriptEngine/Machine/Contexts/ContextPropertyMapper.cs index 96c975743..90d71c9a2 100644 --- a/src/ScriptEngine/Machine/Contexts/ContextPropertyMapper.cs +++ b/src/ScriptEngine/Machine/Contexts/ContextPropertyMapper.cs @@ -17,7 +17,8 @@ namespace ScriptEngine.Machine.Contexts { public class ContextPropertyMapper { - private List> _properties; + // Публикуется последним: кто увидел заполненный список, увидит и словарь имен + private volatile List> _properties; private IdentifiersTrie _propertyNumbers; private readonly object _locker = new object(); diff --git a/src/ScriptEngine/Machine/GlobalInstancesManager.cs b/src/ScriptEngine/Machine/GlobalInstancesManager.cs index 94a8d607e..e951d9a9f 100644 --- a/src/ScriptEngine/Machine/GlobalInstancesManager.cs +++ b/src/ScriptEngine/Machine/GlobalInstancesManager.cs @@ -7,6 +7,7 @@ This Source Code Form is subject to the terms of the using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -14,7 +15,9 @@ namespace ScriptEngine.Machine { public class GlobalInstancesManager : IGlobalsManager { - private readonly Dictionary _instances = new Dictionary(); + // Экземпляры добавляются и во время работы (ПодключитьВнешнююКомпоненту, ЗагрузитьБиблиотеку), + // пока другие потоки их читают + private readonly ConcurrentDictionary _instances = new ConcurrentDictionary(); public void Dispose() { @@ -30,12 +33,13 @@ public void Dispose() public void RegisterInstance(object instance) { - _instances.Add(instance.GetType(), instance); + RegisterInstance(instance.GetType(), instance); } public void RegisterInstance(Type type, object instance) { - _instances.Add(type, instance); + if (!_instances.TryAdd(type, instance)) + throw new ArgumentException($"An item with the same key has already been added. Key: {type}"); } public object GetInstance(Type type) diff --git a/src/Tests/OneScript.Core.Tests/TemplateStorageTests.cs b/src/Tests/OneScript.Core.Tests/TemplateStorageTests.cs new file mode 100644 index 000000000..9a4930ab9 --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/TemplateStorageTests.cs @@ -0,0 +1,72 @@ +/*---------------------------------------------------------- +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 Moq; +using OneScript.StandardLibrary.Binary; +using ScriptEngine.HostedScript; +using Xunit; + +namespace OneScript.Core.Tests +{ + public class TemplateStorageTests + { + [Fact] + public void TemplatesRegisteredFromManyThreadsAreKept() + { + const int threadsCount = 8; + const int templatesPerThread = 500; + var storage = new TemplateStorage(Mock.Of()); + var errors = new ConcurrentQueue(); + + // Так регистрируют макеты библиотеки, загружаемые из разных заданий + var threads = Enumerable.Range(0, threadsCount).Select(number => new Thread(() => + { + try + { + for (var i = 0; i < templatesPerThread; i++) + { + storage.RegisterTemplate($"Макет{number}_{i}", new StubTemplate()); + } + } + catch (Exception e) + { + errors.Enqueue(e); + } + })).ToArray(); + + foreach (var thread in threads) + { + thread.Start(); + } + foreach (var thread in threads) + { + thread.Join(); + } + + errors.Should().BeEmpty(); + storage.GetTemplates().Should().HaveCount(threadsCount * templatesPerThread); + } + + private class StubTemplate : ITemplate + { + public string GetFilename() => ""; + + public BinaryDataContext GetBinaryData() => throw new NotSupportedException(); + + public TemplateKind Kind => TemplateKind.File; + + public void Dispose() + { + } + } + } +} diff --git a/src/Tests/OneScript.DebugProtocol.Test/BreakpointManagerTests.cs b/src/Tests/OneScript.DebugProtocol.Test/BreakpointManagerTests.cs new file mode 100644 index 000000000..08e60039c --- /dev/null +++ b/src/Tests/OneScript.DebugProtocol.Test/BreakpointManagerTests.cs @@ -0,0 +1,101 @@ +/*---------------------------------------------------------- +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.DebugServices; +using Xunit; + +namespace OneScript.DebugProtocol.Test +{ + public class BreakpointManagerTests + { + [Fact] + public void ConditionOfRemovedBreakpointIsNull() + { + var manager = new DefaultBreakpointManager(); + manager.SetBreakpoints("module.os", new[] { (10, "А = 1") }); + manager.FindBreakpoint("module.os", 10).Should().BeTrue(); + + // Отладчик снял точку между проверкой строки и чтением условия + manager.Clear(); + + manager.GetCondition("module.os", 10).Should().BeNull(); + } + + [Fact] + public void BreakpointsCanBeCheckedWhileDebuggerChangesThem() + { + const string module = "module.os"; + var manager = new DefaultBreakpointManager(); + var errors = new ConcurrentQueue(); + var stop = false; + const int scriptThreadsCount = 4; + using var readersRunning = new CountdownEvent(scriptThreadsCount); + + // Потоки скриптов проверяют точки на каждой строке + var scriptThreads = Enumerable.Range(0, scriptThreadsCount).Select(_ => new Thread(() => + { + var signaled = false; + try + { + while (!Volatile.Read(ref stop)) + { + for (var line = 1; line <= 20; line++) + { + if (manager.FindBreakpoint(module, line)) + manager.GetCondition(module, line); + } + manager.StopOnAnyException("ошибка"); + + if (!signaled) + { + signaled = true; + readersRunning.Signal(); + } + } + } + catch (Exception e) + { + errors.Enqueue(e); + } + finally + { + if (!signaled) + readersRunning.Signal(); + } + }) { IsBackground = true }).ToArray(); + + foreach (var thread in scriptThreads) + { + thread.Start(); + } + + // Поток отладчика меняет точки, когда все потоки скриптов уже проверяют их + var readersStarted = readersRunning.Wait(TimeSpan.FromSeconds(10)); + var breakpoints = Enumerable.Range(1, 20).Select(line => (line, "")).ToArray(); + for (var i = 0; i < 20000; i++) + { + manager.SetBreakpoints(module, breakpoints); + manager.SetExceptionBreakpoints(new[] { ("all", "ошибка") }); + manager.Clear(); + } + + Volatile.Write(ref stop, true); + foreach (var thread in scriptThreads) + { + thread.Join(); + } + + readersStarted.Should().BeTrue(); + errors.Should().BeEmpty(); + } + } +} diff --git a/src/oscript/ConsoleHostImpl.cs b/src/oscript/ConsoleHostImpl.cs index 8ff35e2b1..8b618bc68 100644 --- a/src/oscript/ConsoleHostImpl.cs +++ b/src/oscript/ConsoleHostImpl.cs @@ -13,7 +13,19 @@ namespace oscript { internal static class ConsoleHostImpl { + // Цвет консоли общий для процесса: смена цвета, вывод и возврат цвета + // из разных потоков не должны перемежаться + private static readonly object EchoLock = new object(); + public static void Echo(string text, MessageStatusEnum status = MessageStatusEnum.Ordinary) + { + lock (EchoLock) + { + EchoInternal(text, status); + } + } + + private static void EchoInternal(string text, MessageStatusEnum status) { if (status == MessageStatusEnum.Ordinary) {