From bdb9ee265ab9dcc769a72de5a20cd90a4328ff3f Mon Sep 17 00:00:00 2001 From: sfaqer Date: Fri, 25 Sep 2026 12:38:26 +1000 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=BE=D0=BD=D0=BE=D0=B2=D0=BE=D0=B5?= =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5.=D0=9E=D1=82=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=82=D1=8C()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Отмена прерывает код задания перед очередной строкой (стековая машина), на итерации цикла и входе в метод (натив), а также Приостановить(), ожидание других заданий и БлокировкаРесурса.Заблокировать(). Попыткой не перехватывается, обработчики ПриЗавершении отрабатывают. Новое состояние СостояниеФоновогоЗадания.Отменено. Блокировки, не отпущенные процессом (отмена, необработанная ошибка), освобождаются при его завершении. Добавлен Заблокировать(Таймаут). ОжидатьЗавершенияЗадач() внутри задания ждет только задания, запущенные из него: раньше задание ждало само себя, а родитель и соседи - друг друга. Задание, где метод встроенного объекта бросил исключение .NET, завершается аварийно, а не остается активным. Задания ставятся в общую очередь пула, чтобы ожидание из потока пула (запрос веб-сервера) не выполняло задание в своем потоке: иначе задание входит в блокировки ожидающего и отпускает их при завершении. Co-Authored-By: Claude Opus 5.5 --- .../Execution/BslProcessExtensions.cs | 25 + src/OneScript.Core/Execution/IBslProcess.cs | 7 + .../Execution/IBslProcessFactory.cs | 6 + .../Compiler/ExpressionHelpers.cs | 21 + .../Compiler/MethodCompiler.cs | 27 +- .../Runtime/CallableMethod.cs | 3 + .../StandardGlobalContext.cs | 14 +- .../Tasks/BackgroundTask.cs | 85 ++- .../Tasks/BackgroundTasksManager.cs | 40 +- .../Tasks/TaskStateEnum.cs | 4 +- src/ScriptEngine/BslProcess.cs | 31 +- src/ScriptEngine/BslProcessFactory.cs | 6 +- .../Contexts/CriticalSectionContext.cs | 50 +- .../Machine/Contexts/ProcessResourceLocks.cs | 50 ++ src/ScriptEngine/Machine/MachineInstance.cs | 6 + .../BackgroundTaskThreadTests.cs | 58 ++ tests/tasks.os | 576 ++++++++++++++++++ 17 files changed, 979 insertions(+), 30 deletions(-) create mode 100644 src/OneScript.Core/Execution/BslProcessExtensions.cs create mode 100644 src/ScriptEngine/Machine/Contexts/ProcessResourceLocks.cs create mode 100644 src/Tests/OneScript.Core.Tests/BackgroundTaskThreadTests.cs diff --git a/src/OneScript.Core/Execution/BslProcessExtensions.cs b/src/OneScript.Core/Execution/BslProcessExtensions.cs new file mode 100644 index 000000000..debc45c9b --- /dev/null +++ b/src/OneScript.Core/Execution/BslProcessExtensions.cs @@ -0,0 +1,25 @@ +/*---------------------------------------------------------- +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; + +namespace OneScript.Execution +{ + public static class BslProcessExtensions + { + /// + /// Исключение прерывает процесс по запросу отмены. + /// Такое исключение не должно перехватываться bsl-кодом и превращаться в ScriptException. + /// + public static bool IsCancellationOf(this Exception exception, IBslProcess process) + { + return exception is OperationCanceledException canceled + && canceled.CancellationToken.IsCancellationRequested + && canceled.CancellationToken == process.CancellationToken; + } + } +} diff --git a/src/OneScript.Core/Execution/IBslProcess.cs b/src/OneScript.Core/Execution/IBslProcess.cs index 297f11f63..d8eb87f13 100644 --- a/src/OneScript.Core/Execution/IBslProcess.cs +++ b/src/OneScript.Core/Execution/IBslProcess.cs @@ -8,6 +8,7 @@ This Source Code Form is subject to the terms of the #nullable enable using System; +using System.Threading; using OneScript.Contexts; using OneScript.DependencyInjection; using OneScript.Values; @@ -40,6 +41,12 @@ public interface IBslProcess public int VirtualThreadId { get; } + /// + /// Токен отмены исполнения. При запросе отмены исполнитель прерывает bsl-код + /// исключением с этим токеном. + /// + public CancellationToken CancellationToken => CancellationToken.None; + /** * Обертка текущего процесса в терминах BSL. * Может отсутствовать, полностью определяется хост-приложением процесса. diff --git a/src/OneScript.Core/Execution/IBslProcessFactory.cs b/src/OneScript.Core/Execution/IBslProcessFactory.cs index e2679a94f..58226f6c8 100644 --- a/src/OneScript.Core/Execution/IBslProcessFactory.cs +++ b/src/OneScript.Core/Execution/IBslProcessFactory.cs @@ -5,6 +5,8 @@ This Source Code Form is subject to the terms of the at http://mozilla.org/MPL/2.0/. ----------------------------------------------------------*/ +using System.Threading; + namespace OneScript.Execution { /// @@ -14,5 +16,9 @@ public interface IBslProcessFactory { // Создать новый bsl-процесс с пустым стеком вызовов IBslProcess NewProcess(); + + // Создать новый bsl-процесс, исполнение которого можно отменить через токен. + // Фабрики, которые не поддерживают отмену, создают обычный процесс + IBslProcess NewProcess(CancellationToken cancellationToken) => NewProcess(); } } \ No newline at end of file diff --git a/src/OneScript.Native/Compiler/ExpressionHelpers.cs b/src/OneScript.Native/Compiler/ExpressionHelpers.cs index 8ed0f479e..e686b5dd0 100644 --- a/src/OneScript.Native/Compiler/ExpressionHelpers.cs +++ b/src/OneScript.Native/Compiler/ExpressionHelpers.cs @@ -12,8 +12,10 @@ This Source Code Form is subject to the terms of the using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; +using System.Threading; using Microsoft.CSharp.RuntimeBinder; using OneScript.Contexts; +using OneScript.Execution; using OneScript.Language.LexicalAnalysis; using OneScript.Localization; using OneScript.Native.Runtime; @@ -679,6 +681,25 @@ public static Expression InvokeBslNativeMethod(BslNativeMethodInfo nativeMethod, PackArgsToArgsArray(args)); } + public static Expression GetCancellationToken(ParameterExpression process) + { + return Expression.Property(process, nameof(IBslProcess.CancellationToken)); + } + + public static Expression ThrowIfCancellationRequested(Expression cancellationToken) + { + return Expression.Call(cancellationToken, nameof(CancellationToken.ThrowIfCancellationRequested), Type.EmptyTypes); + } + + public static Expression IsCancellationOf(Expression exception, ParameterExpression process) + { + var method = OperationsCache.GetOrAdd( + typeof(BslProcessExtensions), + nameof(BslProcessExtensions.IsCancellationOf)); + + return Expression.Call(method, exception, process); + } + private static Expression PackArgsToArgsArray(IEnumerable args) { return Expression.NewArrayInit(typeof(BslValue), args.Select(ConvertToBslValue)); diff --git a/src/OneScript.Native/Compiler/MethodCompiler.cs b/src/OneScript.Native/Compiler/MethodCompiler.cs index a2be74b8c..636ef3ba7 100644 --- a/src/OneScript.Native/Compiler/MethodCompiler.cs +++ b/src/OneScript.Native/Compiler/MethodCompiler.cs @@ -13,6 +13,7 @@ This Source Code Form is subject to the terms of the using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Threading; using Microsoft.CSharp.RuntimeBinder; using OneScript.Commons; using OneScript.Compilation.Binding; @@ -824,7 +825,9 @@ protected override void VisitWhileNode(WhileLoopNode node) var block = _blocks.LeaveBlock(); + var cancellationToken = Expression.Variable(typeof(CancellationToken)); var result = new List(); + result.Add(ExpressionHelpers.ThrowIfCancellationRequested(cancellationToken)); result.Add(Expression.IfThen( Expression.Not(block.BuildStack.Pop()), Expression.Break(block.LoopBreak))); @@ -832,7 +835,13 @@ protected override void VisitWhileNode(WhileLoopNode node) result.AddRange(block.GetStatements()); var loop = Expression.Loop(Expression.Block(result), block.LoopBreak, block.LoopContinue); - _blocks.Add(loop); + _blocks.Add(Expression.Block(new[] {cancellationToken}, new Expression[] {ReadCancellationToken(cancellationToken), loop})); + } + + // Токен читается один раз перед циклом, а не у процесса на каждой итерации + private BinaryExpression ReadCancellationToken(ParameterExpression cancellationToken) + { + return Expression.Assign(cancellationToken, ExpressionHelpers.GetCancellationToken(_processParameter)); } protected override void VisitWhileCondition(BslSyntaxNode node) @@ -871,12 +880,15 @@ protected override void VisitForLoopNode(ForLoopNode node) var initialValue = block.BuildStack.Pop(); var counterVar = block.BuildStack.Pop(); + var cancellationToken = Expression.Variable(typeof(CancellationToken)); var result = new List(); + result.Add(ReadCancellationToken(cancellationToken)); result.Add(Expression.Assign(counterVar, ExpressionHelpers.CreateAssignmentSource(initialValue, counterVar.Type))); var finalVar = Expression.Variable(typeof(decimal)); // TODO: BslNumericValue ? result.Add(Expression.Assign(finalVar, upperLimit)); var loop = new List(); + loop.Add(ExpressionHelpers.ThrowIfCancellationRequested(cancellationToken)); loop.Add(Expression.IfThen( Expression.GreaterThan(ExpressionHelpers.ToNumber(counterVar), finalVar), Expression.Break(block.LoopBreak))); @@ -888,7 +900,7 @@ protected override void VisitForLoopNode(ForLoopNode node) result.Add(Expression.Loop(Expression.Block(loop), block.LoopBreak)); - _blocks.Add(Expression.Block(new[] {finalVar}, result)); + _blocks.Add(Expression.Block(new[] {finalVar, cancellationToken}, result)); } protected override void VisitForInitializer(BslSyntaxNode node) @@ -950,7 +962,9 @@ protected override void VisitForEachLoopNode(ForEachLoopNode node) var getEnumeratorInvoke = Expression.Call(collectionCast, getEnumeratorMethod); var enumeratorVar = Expression.Variable(typeof(IEnumerator)); + var cancellationToken = Expression.Variable(typeof(CancellationToken)); var result = new List(); + result.Add(ReadCancellationToken(cancellationToken)); result.Add(Expression.Assign(enumeratorVar, getEnumeratorInvoke)); @@ -963,6 +977,7 @@ protected override void VisitForEachLoopNode(ForEachLoopNode node) typeof(BslValue)) ); + loop.Add(ExpressionHelpers.ThrowIfCancellationRequested(cancellationToken)); loop.Add(assignCurrent); loop.AddRange(block.GetStatements()); @@ -975,7 +990,7 @@ protected override void VisitForEachLoopNode(ForEachLoopNode node) result.Add(finalLoop); - _blocks.Add(Expression.Block(new[] {enumeratorVar}, result)); + _blocks.Add(Expression.Block(new[] {enumeratorVar, cancellationToken}, result)); } protected override void VisitIteratorLoopVariable(TerminalNode node) @@ -1012,8 +1027,12 @@ protected override void VisitTryExceptNode(TryExceptNode node) var except = block.BuildStack.Pop(); var tryBlock = block.BuildStack.Pop(); + // Отмена процесса не должна перехватываться Попыткой + var notCancellation = Expression.Not( + ExpressionHelpers.IsCancellationOf(block.CurrentException, _processParameter)); + _blocks.Add(Expression.TryCatch(tryBlock, - Expression.Catch(block.CurrentException, except)) + Expression.Catch(block.CurrentException, except, notCancellation)) ); } diff --git a/src/OneScript.Native/Runtime/CallableMethod.cs b/src/OneScript.Native/Runtime/CallableMethod.cs index f61237aad..3d77aeb12 100644 --- a/src/OneScript.Native/Runtime/CallableMethod.cs +++ b/src/OneScript.Native/Runtime/CallableMethod.cs @@ -43,6 +43,9 @@ public BslValue Invoke(IBslProcess process, object target, BslValue[] args) throw new InvalidOperationException($"Method {_method} was not compiled"); } + // Проверка на входе в метод прерывает и рекурсию без циклов + process.CancellationToken.ThrowIfCancellationRequested(); + var callableWrapper = GetCallableWrapper(target); return _delegate.Invoke(callableWrapper, args, process); } diff --git a/src/OneScript.StandardLibrary/StandardGlobalContext.cs b/src/OneScript.StandardLibrary/StandardGlobalContext.cs index 26fc2f349..f694a29e9 100644 --- a/src/OneScript.StandardLibrary/StandardGlobalContext.cs +++ b/src/OneScript.StandardLibrary/StandardGlobalContext.cs @@ -99,12 +99,22 @@ public ExecutionThreadContext CurrentThread(IBslProcess process) /// /// Приостанавливает выполнение скрипта. + /// Ожидание прерывается, если фоновое задание, в котором оно выполняется, отменено. /// + /// Текущий процесс, в котором вызван данный метод /// Время приостановки в миллисекундах [ContextMethod("Приостановить", "Sleep")] - public void Sleep(int delay) + public void Sleep(IBslProcess process, int delay) { - System.Threading.Thread.Sleep(delay); + var cancellationToken = process.CancellationToken; + + // WaitHandle дороже Thread.Sleep и создает событие ядра, а Sleep(0) еще и уступает квант времени + if (delay == 0 || !cancellationToken.CanBeCanceled) + System.Threading.Thread.Sleep(delay); + else + cancellationToken.WaitHandle.WaitOne(delay); + + cancellationToken.ThrowIfCancellationRequested(); } [ContextMethod("КраткоеПредставлениеОшибки", "BriefErrorDescription")] diff --git a/src/OneScript.StandardLibrary/Tasks/BackgroundTask.cs b/src/OneScript.StandardLibrary/Tasks/BackgroundTask.cs index d3dd251e3..47e554530 100644 --- a/src/OneScript.StandardLibrary/Tasks/BackgroundTask.cs +++ b/src/OneScript.StandardLibrary/Tasks/BackgroundTask.cs @@ -7,6 +7,7 @@ This Source Code Form is subject to the terms of the using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using OneScript.Commons; using OneScript.Contexts; @@ -24,6 +25,9 @@ public class BackgroundTask : AutoContext { private readonly BslMethodInfo _method; private readonly int _methIndex; + private readonly CancellationTokenSource _cancellation = new CancellationTokenSource(); + private readonly object _cancellationLock = new object(); + private bool _cancellationReleased; private Task _workerTask; private int _taskId; @@ -52,6 +56,24 @@ public Task WorkerTask public int TaskId => _taskId; + /// + /// Задание, из которого запущено это, или null, если запущено не из фонового задания + /// + internal BackgroundTask Parent { get; set; } + + internal bool IsDescendantOf(BackgroundTask ancestor) + { + for (var task = Parent; task != null; task = task.Parent) + { + if (ReferenceEquals(task, ancestor)) + return true; + } + + return false; + } + + public CancellationToken CancellationToken => _cancellation.Token; + [ContextProperty("УникальныйИдентификатор","UUID")] public GuidWrapper Identifier { get; private set; } @@ -76,21 +98,69 @@ public Task WorkerTask /// /// Ждать завершения задания указанное число миллисекунд /// + /// Текущий процесс, в котором вызван данный метод /// Таймаут в миллисекундах. Если ноль - ждать вечно /// Истина - дождались завершения. Ложь - сработал таймаут [ContextMethod("ОжидатьЗавершения", "Wait")] - public bool Wait(int timeout = 0) + public bool Wait(IBslProcess process, int timeout = 0) { timeout = BackgroundTasksManager.ConvertTimeout(timeout); - return WorkerTask.Wait(timeout); + return WorkerTask.Wait(timeout, process.CancellationToken); } + /// + /// Отменяет выполнение задания. Код задания прерывается перед выполнением очередной строки + /// или во время Приостановить(), перехватить отмену через Попытка нельзя. + /// Обработчики завершения потока исполнения при этом отрабатывают. + /// Метод не дожидается остановки задания, для этого используйте ОжидатьЗавершения(). + /// Отмена завершенного задания ничего не делает. + /// + [ContextMethod("Отменить", "Cancel")] + public void Cancel() + { + lock (_cancellationLock) + { + // Источник отмены освобождается, когда задание завершилось + if (!_cancellationReleased) + _cancellation.Cancel(); + } + } + public void ExecuteOnCurrentThread(IBslProcess process) + { + try + { + Execute(process); + } + finally + { + ReleaseCancellation(); + } + } + + // Ожидание в Приостановить() создает у источника отмены событие ядра, + // а задание остается в списке менеджера до Очистить() + private void ReleaseCancellation() + { + lock (_cancellationLock) + { + _cancellationReleased = true; + _cancellation.Dispose(); + } + } + + private void Execute(IBslProcess process) { if (State != TaskStateEnum.NotRunned) throw new RuntimeException(Locale.NStr("ru = 'Неверное состояние задачи';en = 'Incorrect task status'")); + if (_cancellation.IsCancellationRequested) + { + State = TaskStateEnum.Canceled; + return; + } + var parameters = Parameters is ArrayImpl array ? array.ToArray() : Array.Empty(); @@ -117,6 +187,17 @@ public void ExecuteOnCurrentThread(IBslProcess process) ExceptionInfo = new ExceptionInfoContext(exception); } + catch (OperationCanceledException exception) when (exception.CancellationToken == _cancellation.Token) + { + State = TaskStateEnum.Canceled; + } + catch (Exception exception) + { + // Метод встроенного объекта бросает исключения .NET, а не ScriptException: + // без этого задание осталось бы «Активно» без информации об ошибке + State = TaskStateEnum.CompletedWithErrors; + ExceptionInfo = new ExceptionInfoContext(new ExternalSystemException(exception)); + } } } } diff --git a/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs b/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs index c3d253ce2..4a65990f6 100644 --- a/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs +++ b/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs @@ -46,12 +46,17 @@ public BackgroundTasksManager(ExecutionContext runtimeContext) [ContextMethod("Выполнить", "Execute")] public BackgroundTask Execute(IRuntimeContextInstance target, string methodName, ArrayImpl parameters = null, bool longRunning = false) { - var task = new BackgroundTask(target, methodName, parameters); + var task = new BackgroundTask(target, methodName, parameters) + { + Parent = FindCurrentTask() + }; - var taskCreationOptions = longRunning ? TaskCreationOptions.LongRunning : TaskCreationOptions.None; + // PreferFairness - в общую очередь пула: иначе ожидающий из потока пула (запрос веб-сервера) + // может выполнить задание прямо в своем потоке, и два процесса поделят блокировки потока + var taskCreationOptions = longRunning ? TaskCreationOptions.LongRunning : TaskCreationOptions.PreferFairness; var worker = new Task(() => { - var process = _runtimeContext.Services.Resolve().NewProcess(); + var process = _runtimeContext.Services.Resolve().NewProcess(task.CancellationToken); task.ExecuteOnCurrentThread(process); }, taskCreationOptions); @@ -72,11 +77,12 @@ public void Clear() /// /// Ожидает завершения всех переданных заданий /// + /// Текущий процесс, в котором вызван данный метод /// Массив заданий /// Таймаут ожидания. 0 = ожидать бесконечно /// Истина - дождались все задания, Ложь - истек таймаут [ContextMethod("ОжидатьВсе", "WaitAll")] - public bool WaitAll(ArrayImpl tasks, int timeout = 0) + public bool WaitAll(IBslProcess process, ArrayImpl tasks, int timeout = 0) { var workers = GetWorkerTasks(tasks); timeout = ConvertTimeout(timeout); @@ -84,17 +90,18 @@ public bool WaitAll(ArrayImpl tasks, int timeout = 0) // Фоновые задания перехватывают исключения внутри себя // и выставляют свойство ИнформацияОбОшибке // если WaitAll выбросит исключение, значит действительно что-то пошло не так на уровне самого Task - return Task.WaitAll(workers, timeout); + return Task.WaitAll(workers, timeout, process.CancellationToken); } /// /// Ожидать хотя бы одно из переданных заданий. /// + /// Текущий процесс, в котором вызван данный метод /// Массив заданий /// Таймаут ожидания. 0 = ожидать бесконечно /// Число. Индекс в массиве заданий, указывающий на элемент-задание, которое завершилось. -1 = сработал таймаут [ContextMethod("ОжидатьЛюбое", "WaitAny")] - public int WaitAny(ArrayImpl tasks, int timeout = 0) + public int WaitAny(IBslProcess process, ArrayImpl tasks, int timeout = 0) { var workers = GetWorkerTasks(tasks); timeout = ConvertTimeout(timeout); @@ -102,19 +109,22 @@ public int WaitAny(ArrayImpl tasks, int timeout = 0) // Фоновые задания перехватывают исключения внутри себя // и выставляют свойство ИнформацияОбОшибке // если WaitAny выбросит исключение, значит действительно что-то пошло не так на уровне самого Task - return Task.WaitAny(workers, timeout); + return Task.WaitAny(workers, timeout, process.CancellationToken); } /// /// Блокирует поток до завершения всех заданий. + /// Вызванный из фонового задания, ждет только задания, запущенные из него (и из них дальше). /// Выбрасывает исключение, если какие-то задания завершились аварийно. /// Выброшенное исключение в свойстве Параметры содержит массив аварийных заданий. /// [ContextMethod("ОжидатьЗавершенияЗадач", "WaitCompletionOfTasks")] - public void WaitCompletionOfTasks() + public void WaitCompletionOfTasks(IBslProcess process) { - var snapshot = _tasks.Values.ToArray(); - Task.WaitAll(GetWorkerTasks(snapshot)); + // Родитель и соседние задания могут сами ждать текущее - их ждать нельзя + var current = FindCurrentTask(); + var snapshot = _tasks.Values.Where(x => current == null || x.IsDescendantOf(current)).ToArray(); + Task.WaitAll(GetWorkerTasks(snapshot), process.CancellationToken); var failedTasks = snapshot.Where(x => x.State == TaskStateEnum.CompletedWithErrors) .ToList(); @@ -181,15 +191,21 @@ public ArrayImpl GetBackgroundJobs(StructureImpl filter = default) [ContextMethod("ПолучитьТекущее", "GetCurrent")] public IValue GetCurrent() + { + return (IValue)FindCurrentTask() ?? ValueFactory.Create(); + } + + // Фоновое задание, в котором выполняется код, или null + private BackgroundTask FindCurrentTask() { var currentId = Task.CurrentId; if (currentId == null) - return ValueFactory.Create(); + return null; if (_tasks.TryGetValue(currentId.Value, out var task) && task.State == TaskStateEnum.Running) return task; - return ValueFactory.Create(); + return null; } internal static int ConvertTimeout(int timeout) diff --git a/src/OneScript.StandardLibrary/Tasks/TaskStateEnum.cs b/src/OneScript.StandardLibrary/Tasks/TaskStateEnum.cs index 970dd14e6..18448349f 100644 --- a/src/OneScript.StandardLibrary/Tasks/TaskStateEnum.cs +++ b/src/OneScript.StandardLibrary/Tasks/TaskStateEnum.cs @@ -19,6 +19,8 @@ public enum TaskStateEnum [EnumValue("Завершено", "Completed")] Completed, [EnumValue("ЗавершеноАварийно", "Failed")] - CompletedWithErrors + CompletedWithErrors, + [EnumValue("Отменено", "Canceled")] + Canceled } } \ No newline at end of file diff --git a/src/ScriptEngine/BslProcess.cs b/src/ScriptEngine/BslProcess.cs index 4d7deeb06..8b735ba09 100644 --- a/src/ScriptEngine/BslProcess.cs +++ b/src/ScriptEngine/BslProcess.cs @@ -8,11 +8,14 @@ This Source Code Form is subject to the terms of the using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using OneScript.Contexts; using OneScript.DependencyInjection; using OneScript.Execution; using OneScript.Values; using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; +using ExecutionContext = ScriptEngine.Machine.ExecutionContext; namespace ScriptEngine { @@ -25,9 +28,13 @@ internal class BslProcess : IBslProcess private bool _isRunning; private bool _disposed; + private CancellationToken _cancellationToken; + private ProcessResourceLocks _resourceLocks; - public BslProcess(int id, ExecutionContext context, IEnumerable executorProviders) + public BslProcess(int id, ExecutionContext context, IEnumerable executorProviders, + CancellationToken cancellationToken) { + _cancellationToken = cancellationToken; _executorProviders = executorProviders.ToArray(); _bslExecutorsByModule = _executorProviders.ToDictionary(item => item.SupportedModuleType, item => item.GetInvokeDelegate()); @@ -40,6 +47,10 @@ public BslProcess(int id, ExecutionContext context, IEnumerable _cancellationToken; + + internal ProcessResourceLocks ResourceLocks => _resourceLocks ??= new ProcessResourceLocks(); + public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMethodInfo method, IValue[] arguments) { var notifyExecutors = !_isRunning; @@ -58,12 +69,22 @@ public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMe { if (notifyExecutors) { - RaiseTerminationEvent(); - if (BslWrapper is IDisposable disposable) + // Обработчики завершения должны отработать и у отмененного процесса + _cancellationToken = CancellationToken.None; + try { - disposable.Dispose(); + RaiseTerminationEvent(); + if (BslWrapper is IDisposable disposable) + { + disposable.Dispose(); + } } - + finally + { + // Монитор привязан к потоку: не отпущенные процессом блокировки больше никто не освободит + _resourceLocks?.ReleaseAll(); + } + Array.ForEach(_executorProviders, e => e.AfterProcessExit(this)); Services.Dispose(); _isRunning = false; diff --git a/src/ScriptEngine/BslProcessFactory.cs b/src/ScriptEngine/BslProcessFactory.cs index 8d063ab74..39005e5d6 100644 --- a/src/ScriptEngine/BslProcessFactory.cs +++ b/src/ScriptEngine/BslProcessFactory.cs @@ -25,13 +25,15 @@ public BslProcessFactory(IServiceContainer services) _services = services; } - public IBslProcess NewProcess() + public IBslProcess NewProcess() => NewProcess(CancellationToken.None); + + public IBslProcess NewProcess(CancellationToken cancellationToken) { // Создаем новый контекст со всеми зависимостями var context = _services.Resolve(); var executors = _services.ResolveEnumerable(); - return new BslProcess(Interlocked.Increment(ref _threadIdCounter), context, executors); + return new BslProcess(Interlocked.Increment(ref _threadIdCounter), context, executors, cancellationToken); } } } \ No newline at end of file diff --git a/src/ScriptEngine/Machine/Contexts/CriticalSectionContext.cs b/src/ScriptEngine/Machine/Contexts/CriticalSectionContext.cs index c1b84fb04..6b161b69b 100644 --- a/src/ScriptEngine/Machine/Contexts/CriticalSectionContext.cs +++ b/src/ScriptEngine/Machine/Contexts/CriticalSectionContext.cs @@ -6,8 +6,11 @@ This Source Code Form is subject to the terms of the ----------------------------------------------------------*/ using System; +using System.Diagnostics; using System.Threading; using OneScript.Contexts; +using OneScript.Exceptions; +using OneScript.Execution; namespace ScriptEngine.Machine.Contexts { @@ -17,6 +20,9 @@ namespace ScriptEngine.Machine.Contexts [ContextClass("БлокировкаРесурса", "ResourceLock")] public class CriticalSectionContext : AutoContext, IDisposable { + // Monitor не принимает токен отмены, поэтому ожидание блокировки идет порциями + private const int CancellationPollInterval = 50; + private object _lockObject; private CriticalSectionContext() @@ -29,10 +35,50 @@ private CriticalSectionContext(object lockObject) _lockObject = lockObject; } + /// + /// Захватывает блокировку, ожидая ее освобождения другими потоками. + /// Ожидание прерывается, если фоновое задание, в котором оно выполняется, отменено. + /// Блокировки, не отпущенные до завершения задания, освобождаются автоматически. + /// + /// Текущий процесс, в котором вызван данный метод + /// Таймаут ожидания в миллисекундах. 0 - ждать бесконечно + /// Истина - блокировка захвачена, Ложь - истек таймаут [ContextMethod("Заблокировать", "Lock")] - public void Lock() + public bool Lock(IBslProcess process, int timeout = 0) + { + if (timeout < 0) + throw RuntimeException.InvalidArgumentValue(); + + var entered = TryEnter(timeout == 0 ? Timeout.Infinite : timeout, process.CancellationToken); + if (entered) + (process as BslProcess)?.ResourceLocks.Entered(_lockObject); + + return entered; + } + + private bool TryEnter(int timeout, CancellationToken cancellationToken) { - Monitor.Enter(_lockObject); + if (!cancellationToken.CanBeCanceled) + return Monitor.TryEnter(_lockObject, timeout); + + if (Monitor.TryEnter(_lockObject)) + return true; + + var start = Stopwatch.GetTimestamp(); + while (true) + { + var wait = timeout == Timeout.Infinite + ? CancellationPollInterval + : (int)Math.Clamp(timeout - Stopwatch.GetElapsedTime(start).TotalMilliseconds, 0, CancellationPollInterval); + + if (Monitor.TryEnter(_lockObject, wait)) + return true; + + cancellationToken.ThrowIfCancellationRequested(); + + if (timeout != Timeout.Infinite && Stopwatch.GetElapsedTime(start).TotalMilliseconds >= timeout) + return false; + } } [ContextMethod("Разблокировать", "Unlock")] diff --git a/src/ScriptEngine/Machine/Contexts/ProcessResourceLocks.cs b/src/ScriptEngine/Machine/Contexts/ProcessResourceLocks.cs new file mode 100644 index 000000000..8063f2de5 --- /dev/null +++ b/src/ScriptEngine/Machine/Contexts/ProcessResourceLocks.cs @@ -0,0 +1,50 @@ +/*---------------------------------------------------------- +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.Collections.Generic; +using System.Threading; + +namespace ScriptEngine.Machine.Contexts +{ + /// + /// Блокировки ресурсов, которые захватывал bsl-процесс. + /// Монитор привязан к потоку: если процесс завершился, не отпустив блокировку + /// (например, фоновое задание отменили), освободить ее из другого потока уже нельзя. + /// Процесс отпускает такие блокировки при завершении, в своем же потоке. + /// Сколько раз блокировка захвачена и отпущена ли она, знает сам монитор, + /// поэтому здесь хранятся только объекты блокировок. + /// + internal class ProcessResourceLocks + { + private readonly List _entries = new List(); + + public void Entered(object lockObject) + { + // Уже отпущенные блокировки не нужны, иначе список рос бы на каждой новой блокировке + _entries.RemoveAll(entry => !Monitor.IsEntered(entry)); + + foreach (var entry in _entries) + { + if (ReferenceEquals(entry, lockObject)) + return; + } + + _entries.Add(lockObject); + } + + public void ReleaseAll() + { + foreach (var lockObject in _entries) + { + while (Monitor.IsEntered(lockObject)) + Monitor.Exit(lockObject); + } + + _entries.Clear(); + } + } +} diff --git a/src/ScriptEngine/Machine/MachineInstance.cs b/src/ScriptEngine/Machine/MachineInstance.cs index 5471c9487..654711d5c 100644 --- a/src/ScriptEngine/Machine/MachineInstance.cs +++ b/src/ScriptEngine/Machine/MachineInstance.cs @@ -490,6 +490,10 @@ private void MainCommandLoop() { throw; } + catch (OperationCanceledException exc) when (exc.IsCancellationOf(_process)) + { + throw; + } catch (ScriptException exc) { exc.SetPositionIfEmpty(GetPositionInfo()); @@ -1298,6 +1302,8 @@ private void RaiseException(int arg) private void LineNum(int arg) { + _process.CancellationToken.ThrowIfCancellationRequested(); + if (_currentFrame.LineNumber != arg) { _currentFrame.LineNumber = arg; diff --git a/src/Tests/OneScript.Core.Tests/BackgroundTaskThreadTests.cs b/src/Tests/OneScript.Core.Tests/BackgroundTaskThreadTests.cs new file mode 100644 index 000000000..364fc6803 --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/BackgroundTaskThreadTests.cs @@ -0,0 +1,58 @@ +/*---------------------------------------------------------- +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.Threading.Tasks; +using OneScript.StandardLibrary; +using ScriptEngine.HostedScript; +using ScriptEngine.HostedScript.Extensions; +using ScriptEngine.Hosting; +using Xunit; + +namespace OneScript.Core.Tests +{ + public class BackgroundTaskThreadTests + { + private const string Script = + "Перем Блок;\n" + + "\n" + + "Функция ВзятьБлокировку() Экспорт\n" + + " Если Блок.Заблокировать(50) Тогда\n" + + " Блок.Разблокировать();\n" + + " Возврат Истина;\n" + + " КонецЕсли;\n" + + " Возврат Ложь;\n" + + "КонецФункции\n" + + "\n" + + "Блок = Новый БлокировкаРесурса;\n" + + "Для Номер = 1 По 20 Цикл\n" + + " Блок.Заблокировать();\n" + + " Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, \"ВзятьБлокировку\");\n" + + " Задание.ОжидатьЗавершения();\n" + + " Блок.Разблокировать();\n" + + " Если Задание.Результат Тогда\n" + + " ВызватьИсключение \"Задание получило блокировку, которую держит ожидающий\";\n" + + " КонецЕсли;\n" + + "КонецЦикла;\n"; + + [Fact] + public void TaskAwaitedFromThreadPoolRunsInItsOwnThread() + { + var builder = DefaultEngineBuilder.Create() + .SetDefaultOptions() + .UseImports() + .UseDefaultHosting() + .SetupEnvironment(e => e.AddStandardLibrary()); + var engine = new HostedScriptEngine(builder.Build()); + engine.Initialize(); + + // Как запрос веб-сервера: процесс без отмены в потоке пула. Если задание выполнится + // в этом же потоке, оно войдет в чужую блокировку и при завершении отпустит ее + Task.Run(() => engine.Engine.AttachedScriptsFactory.LoadFromString( + engine.GetCompilerService(), Script, engine.Engine.NewProcess())).GetAwaiter().GetResult(); + } + } +} diff --git a/tests/tasks.os b/tests/tasks.os index c699008b9..b4a8572ec 100644 --- a/tests/tasks.os +++ b/tests/tasks.os @@ -8,6 +8,9 @@ Перем ЗавершениеПотокаЗафиксировано; Перем ЭлементовВДанныхПриЗавершении; +Перем ЗаданиеЗапущено; +Перем ОтменаПерехвачена; + //////////////////////////////////////////////////////////////////// // Программный интерфейс @@ -42,6 +45,32 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоОбработчикиСобытийВызываютсяВФоновомЗадании"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПараллельноеЧтениеЗначенияСистемногоПеречисления"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПараллельноеЧтениеСвойстваПеречисления"); + ВсеТесты.Добавить("ТестДолжен_ОтменитьВыполняющеесяЗадание"); + ВсеТесты.Добавить("ТестДолжен_ОтменитьЗаданиеСразуПослеЗапуска"); + ВсеТесты.Добавить("ТестДолжен_ПрерватьПриостановитьПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_НеПерехватыватьОтменуВПопытке"); + ВсеТесты.Добавить("ТестДолжен_ВызватьОбработчикЗавершенияПотокаПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_НеМенятьСостояниеЗавершенногоЗаданияПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_ОтменитьЦиклПокаВНативномМодуле"); + ВсеТесты.Добавить("ТестДолжен_ОтменитьЦиклДляВНативномМодуле"); + ВсеТесты.Добавить("ТестДолжен_ОтменитьЦиклДляКаждогоВНативномМодуле"); + ВсеТесты.Добавить("ТестДолжен_ОтменитьРекурсиюВНативномМодуле"); + ВсеТесты.Добавить("ТестДолжен_НеПерехватыватьОтменуВПопыткеНативногоМодуля"); + ВсеТесты.Добавить("ТестДолжен_ПрерватьОжидатьЗавершенияПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_ПрерватьОжидатьВсеПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_ПрерватьОжидатьЛюбоеПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_ПрерватьОжидатьЗавершенияЗадачПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_ПрерватьОжиданиеБлокировкиПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_ОсвободитьБлокировкуОтмененногоЗадания"); + ВсеТесты.Добавить("ТестДолжен_ОсвободитьБлокировкуЗаданияЗавершившегосяСОшибкой"); + ВсеТесты.Добавить("ТестДолжен_ОжидатьЗавершенияЗадачИзЗаданияНеОжидаяСебя"); + ВсеТесты.Добавить("ТестДолжен_ОжидатьЗавершенияЗадачИзЗаданияНеОжидаяРодителя"); + ВсеТесты.Добавить("ТестДолжен_ОжидатьЗавершенияЗадачИзСоседнихЗаданий"); + ВсеТесты.Добавить("ТестДолжен_ЗавершитьАварийноЗаданиеСОшибкойВстроенногоОбъекта"); + ВсеТесты.Добавить("ТестДолжен_ЗахватитьСвободнуюБлокировкуСТаймаутом"); + ВсеТесты.Добавить("ТестДолжен_ВернутьЛожьПоТаймаутуБлокировки"); + ВсеТесты.Добавить("ТестДолжен_ЗахватитьБлокировкуДоИстеченияТаймаута"); + ВсеТесты.Добавить("ТестДолжен_ЗапретитьОтрицательныйТаймаутБлокировки"); Возврат ВсеТесты; @@ -51,6 +80,21 @@ ФоновыеЗадания.Очистить(); КонецПроцедуры +Процедура ПослеЗапускаТеста() Экспорт + // Бесконечные задания упавшего теста не должны пережить его, + // в том числе еще не начавшие выполняться + Незавершенные = Новый Массив; + Для Каждого Задание Из ФоновыеЗадания.ПолучитьФоновыеЗадания() Цикл + Состояние = Задание.Состояние; + Если Состояние = СостояниеФоновогоЗадания.НеВыполнялось + Или Состояние = СостояниеФоновогоЗадания.Активно Тогда + Задание.Отменить(); + Незавершенные.Добавить(Задание); + КонецЕсли; + КонецЦикла; + ФоновыеЗадания.ОжидатьВсе(Незавершенные, 5000); +КонецПроцедуры + Процедура ПроцедураБезПараметров() Экспорт Приостановить(500); КонецПроцедуры @@ -794,3 +838,535 @@ ПозднееЗадание.ОжидатьЗавершения(); КонецПроцедуры + +Процедура БесконечныйЦикл() Экспорт + ЗаданиеЗапущено = Истина; + Пока Истина Цикл + КонецЦикла; +КонецПроцедуры + +Процедура ДолгоеОжидание() Экспорт + ЗаданиеЗапущено = Истина; + Приостановить(60000); +КонецПроцедуры + +Процедура БесконечныйЦиклВПопытке() Экспорт + ЗаданиеЗапущено = Истина; + Пока Истина Цикл + Попытка + Приостановить(10); + Исключение + ОтменаПерехвачена = Истина; + КонецПопытки; + КонецЦикла; +КонецПроцедуры + +Процедура БесконечныйЦиклСОбработчикомЗавершения() Экспорт + ПодписатьсяНаЗавершениеПотока(); + БесконечныйЦикл(); +КонецПроцедуры + +Процедура ДождатьсяЗапускаЗадания() + + Для Итерация = 1 По 1000 Цикл + Если ЗаданиеЗапущено = Истина Тогда + Возврат; + КонецЕсли; + Приостановить(10); + КонецЦикла; + + ВызватьИсключение "Фоновое задание не запустилось"; + +КонецПроцедуры + +Процедура ПроверитьЧтоЗаданиеОтменено(Задание) + + юТест.ПроверитьИстину(Задание.ОжидатьЗавершения(10000), "Отмененное задание должно завершиться"); + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Отменено, Задание.Состояние); + юТест.ПроверитьРавенство(Неопределено, Задание.ИнформацияОбОшибке); + +КонецПроцедуры + +Процедура ТестДолжен_ОтменитьВыполняющеесяЗадание() Экспорт + + ЗаданиеЗапущено = Ложь; + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "БесконечныйЦикл"); + ДождатьсяЗапускаЗадания(); + + Задание.Отменить(); + + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_ОтменитьЗаданиеСразуПослеЗапуска() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "БесконечныйЦикл"); + Задание.Отменить(); + + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_ПрерватьПриостановитьПриОтмене() Экспорт + + ЗаданиеЗапущено = Ложь; + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ДолгоеОжидание"); + ДождатьсяЗапускаЗадания(); + + Задание.Отменить(); + + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_НеПерехватыватьОтменуВПопытке() Экспорт + + ЗаданиеЗапущено = Ложь; + ОтменаПерехвачена = Ложь; + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "БесконечныйЦиклВПопытке"); + ДождатьсяЗапускаЗадания(); + + Задание.Отменить(); + + ПроверитьЧтоЗаданиеОтменено(Задание); + юТест.ПроверитьРавенство(Ложь, ОтменаПерехвачена, "Отмена не должна перехватываться в Попытка"); + +КонецПроцедуры + +Процедура ТестДолжен_ВызватьОбработчикЗавершенияПотокаПриОтмене() Экспорт + + ЗаданиеЗапущено = Ложь; + ЗавершениеПотокаЗафиксировано = Ложь; + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "БесконечныйЦиклСОбработчикомЗавершения"); + ДождатьсяЗапускаЗадания(); + + Задание.Отменить(); + + ПроверитьЧтоЗаданиеОтменено(Задание); + юТест.ПроверитьРавенство(Истина, ЗавершениеПотокаЗафиксировано, + "Обработчик ПриЗавершении должен вызываться и у отмененного задания"); + +КонецПроцедуры + +Процедура ТестДолжен_НеМенятьСостояниеЗавершенногоЗаданияПриОтмене() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "Пустышка"); + Задание.ОжидатьЗавершения(); + + Задание.Отменить(); + + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Состояние); + +КонецПроцедуры + +Функция ЗагрузитьНативныйМодульДляОтмены() + + Текст = + "#native + |Процедура ЦиклПока(Сигнал) Экспорт + | Сигнал.Добавить(Истина); + | Пока Истина Цикл + | КонецЦикла; + |КонецПроцедуры + | + |Процедура ЦиклДля(Сигнал) Экспорт + | Сигнал.Добавить(Истина); + | Для Сч = 1 По 1000000000000 Цикл + | КонецЦикла; + |КонецПроцедуры + | + |Процедура ЦиклДляКаждого(Сигнал) Экспорт + | Элементы = Новый Массив(1000); + | Сигнал.Добавить(Истина); + | Для Каждого А Из Элементы Цикл + | Для Каждого Б Из Элементы Цикл + | Для Каждого В Из Элементы Цикл + | Для Каждого Г Из Элементы Цикл + | КонецЦикла; + | КонецЦикла; + | КонецЦикла; + | КонецЦикла; + |КонецПроцедуры + | + |Функция Фибоначчи(Н) Экспорт + | Если Н < 2 Тогда + | Возврат Н; + | КонецЕсли; + | Возврат Фибоначчи(Н - 1) + Фибоначчи(Н - 2); + |КонецФункции + | + |Процедура Рекурсия(Сигнал) Экспорт + | Сигнал.Добавить(Истина); + | Фибоначчи(100); + |КонецПроцедуры + | + |Процедура ЦиклВПопытке(Сигнал, Перехват) Экспорт + | Сигнал.Добавить(Истина); + | Пока Истина Цикл + | Попытка + | Приостановить(10); + | Исключение + | Перехват.Добавить(Истина); + | КонецПопытки; + | КонецЦикла; + |КонецПроцедуры"; + + Возврат ЗагрузитьСценарийИзСтроки(Текст); + +КонецФункции + +Функция ЗапуститьСоСигналом(Объект, ИмяМетода, ДополнительныйПараметр = Неопределено) + + Сигнал = Новый Массив; + Параметры = Новый Массив; + Параметры.Добавить(Сигнал); + Если ДополнительныйПараметр <> Неопределено Тогда + Параметры.Добавить(ДополнительныйПараметр); + КонецЕсли; + + Задание = ФоновыеЗадания.Выполнить(Объект, ИмяМетода, Параметры); + + Для Итерация = 1 По 1000 Цикл + Если Сигнал.Количество() > 0 Тогда + Прервать; + КонецЕсли; + Приостановить(10); + КонецЦикла; + юТест.ПроверитьРавенство(1, Сигнал.Количество(), "Фоновое задание не запустилось"); + + // После сигнала задание переходит к блокирующему вызову, дадим ему туда дойти + Приостановить(100); + + Возврат Задание; + +КонецФункции + +Функция ЗапуститьИОтменитьНативноеЗадание(ИмяМетода, Перехват = Неопределено) + + Задание = ЗапуститьСоСигналом(ЗагрузитьНативныйМодульДляОтмены(), ИмяМетода, Перехват); + Задание.Отменить(); + + Возврат Задание; + +КонецФункции + +Процедура ТестДолжен_ОтменитьЦиклПокаВНативномМодуле() Экспорт + + Задание = ЗапуститьИОтменитьНативноеЗадание("ЦиклПока"); + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_ОтменитьЦиклДляВНативномМодуле() Экспорт + + Задание = ЗапуститьИОтменитьНативноеЗадание("ЦиклДля"); + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_ОтменитьЦиклДляКаждогоВНативномМодуле() Экспорт + + Задание = ЗапуститьИОтменитьНативноеЗадание("ЦиклДляКаждого"); + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_ОтменитьРекурсиюВНативномМодуле() Экспорт + + Задание = ЗапуститьИОтменитьНативноеЗадание("Рекурсия"); + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_НеПерехватыватьОтменуВПопыткеНативногоМодуля() Экспорт + + Перехват = Новый Массив; + Задание = ЗапуститьИОтменитьНативноеЗадание("ЦиклВПопытке", Перехват); + + ПроверитьЧтоЗаданиеОтменено(Задание); + юТест.ПроверитьРавенство(0, Перехват.Количество(), "Отмена не должна перехватываться в Попытка"); + +КонецПроцедуры + +Процедура ЦиклДоОтмены(Сигнал) Экспорт + Сигнал.Добавить(Истина); + Пока Истина Цикл + Приостановить(10); + КонецЦикла; +КонецПроцедуры + +Процедура ЖдатьДругоеЗадание(Сигнал, Параметры) Экспорт + + Цель = Параметры.Цель; + Задания = Новый Массив; + Задания.Добавить(Цель); + + Если Параметры.Способ = "ОжидатьЗавершенияЗадач" Тогда + // Из задания ждутся только его вложенные задания + ПараметрыВложенного = Новый Массив; + ПараметрыВложенного.Добавить(Новый Массив); + ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЦиклДоОтмены", ПараметрыВложенного); + КонецЕсли; + + Сигнал.Добавить(Истина); + Если Параметры.Способ = "ОжидатьЗавершения" Тогда + Цель.ОжидатьЗавершения(); + ИначеЕсли Параметры.Способ = "ОжидатьВсе" Тогда + ФоновыеЗадания.ОжидатьВсе(Задания); + ИначеЕсли Параметры.Способ = "ОжидатьЛюбое" Тогда + ФоновыеЗадания.ОжидатьЛюбое(Задания); + ИначеЕсли Параметры.Способ = "ОжидатьЗавершенияЗадач" Тогда + ФоновыеЗадания.ОжидатьЗавершенияЗадач(); + КонецЕсли; + +КонецПроцедуры + +Процедура ПроверитьОтменуОжиданияДругогоЗадания(Способ) + + Цель = ЗапуститьСоСигналом(ЭтотОбъект, "ЦиклДоОтмены"); + Ждущее = ЗапуститьСоСигналом(ЭтотОбъект, "ЖдатьДругоеЗадание", Новый Структура("Цель, Способ", Цель, Способ)); + + Ждущее.Отменить(); + Остановилось = Ждущее.ОжидатьЗавершения(10000); + Цель.Отменить(); + + юТест.ПроверитьИстину(Остановилось, "Ожидание другого задания должно прерываться отменой"); + ПроверитьЧтоЗаданиеОтменено(Ждущее); + ПроверитьЧтоЗаданиеОтменено(Цель); + +КонецПроцедуры + +Процедура ТестДолжен_ПрерватьОжидатьЗавершенияПриОтмене() Экспорт + ПроверитьОтменуОжиданияДругогоЗадания("ОжидатьЗавершения"); +КонецПроцедуры + +Процедура ТестДолжен_ПрерватьОжидатьВсеПриОтмене() Экспорт + ПроверитьОтменуОжиданияДругогоЗадания("ОжидатьВсе"); +КонецПроцедуры + +Процедура ТестДолжен_ПрерватьОжидатьЛюбоеПриОтмене() Экспорт + ПроверитьОтменуОжиданияДругогоЗадания("ОжидатьЛюбое"); +КонецПроцедуры + +Процедура ТестДолжен_ПрерватьОжидатьЗавершенияЗадачПриОтмене() Экспорт + ПроверитьОтменуОжиданияДругогоЗадания("ОжидатьЗавершенияЗадач"); +КонецПроцедуры + +Процедура ДержатьБлокировку(Сигнал, Блокировка) Экспорт + + Блокировка.Заблокировать(); + Попытка + Сигнал.Добавить(Истина); + Пока Истина Цикл + Приостановить(10); + КонецЦикла; + Исключение + Блокировка.Разблокировать(); + ВызватьИсключение; + КонецПопытки; + +КонецПроцедуры + +Процедура ВзятьБлокировку(Сигнал, Блокировка) Экспорт + Сигнал.Добавить(Истина); + Блокировка.Заблокировать(); + Блокировка.Разблокировать(); +КонецПроцедуры + +Процедура ВзятьБлокировкуИУпасть(Сигнал, Блокировка) Экспорт + Сигнал.Добавить(Истина); + Блокировка.Заблокировать(); + ВызватьИсключение "Ошибка при захваченной блокировке"; +КонецПроцедуры + +Процедура ПроверитьЧтоБлокировкаСвободна(Блокировка) + + Параметры = Новый Массив; + Параметры.Добавить(Новый Массив); + Параметры.Добавить(Блокировка); + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ВзятьБлокировку", Параметры); + Освободилась = Задание.ОжидатьЗавершения(5000); + Задание.Отменить(); + + юТест.ПроверитьИстину(Освободилась, "Блокировка должна быть свободна"); + +КонецПроцедуры + +Процедура ТестДолжен_ПрерватьОжиданиеБлокировкиПриОтмене() Экспорт + + Блокировка = Новый БлокировкаРесурса; + Держатель = ЗапуститьСоСигналом(ЭтотОбъект, "ДержатьБлокировку", Блокировка); + Ждущее = ЗапуститьСоСигналом(ЭтотОбъект, "ВзятьБлокировку", Блокировка); + + Ждущее.Отменить(); + Остановилось = Ждущее.ОжидатьЗавершения(10000); + Держатель.Отменить(); + + юТест.ПроверитьИстину(Остановилось, "Ожидание блокировки должно прерываться отменой"); + ПроверитьЧтоЗаданиеОтменено(Ждущее); + ПроверитьЧтоЗаданиеОтменено(Держатель); + +КонецПроцедуры + +Процедура ТестДолжен_ОсвободитьБлокировкуОтмененногоЗадания() Экспорт + + Блокировка = Новый БлокировкаРесурса; + Держатель = ЗапуститьСоСигналом(ЭтотОбъект, "ДержатьБлокировку", Блокировка); + + Держатель.Отменить(); + ПроверитьЧтоЗаданиеОтменено(Держатель); + + ПроверитьЧтоБлокировкаСвободна(Блокировка); + +КонецПроцедуры + +Процедура ТестДолжен_ОсвободитьБлокировкуЗаданияЗавершившегосяСОшибкой() Экспорт + + Блокировка = Новый БлокировкаРесурса; + Задание = ЗапуститьСоСигналом(ЭтотОбъект, "ВзятьБлокировкуИУпасть", Блокировка); + Задание.ОжидатьЗавершения(); + + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.ЗавершеноАварийно, Задание.Состояние); + ПроверитьЧтоБлокировкаСвободна(Блокировка); + +КонецПроцедуры + +Функция ЗапуститьВложенноеИДождатьсяВсехЗадач() Экспорт + + Вложенное = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПроцедураБезПараметров"); + ФоновыеЗадания.ОжидатьЗавершенияЗадач(); + + Результат = Новый Структура; + Результат.Вставить("СостояниеВложенного", Вложенное.Состояние); + Результат.Вставить("ТекущееВРеестре", ФоновыеЗадания.ПолучитьТекущее() <> Неопределено); + + Возврат Результат; + +КонецФункции + +Процедура ТестДолжен_ОжидатьЗавершенияЗадачИзЗаданияНеОжидаяСебя() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗапуститьВложенноеИДождатьсяВсехЗадач"); + Завершилось = Задание.ОжидатьЗавершения(10000); + Задание.Отменить(); + + юТест.ПроверитьИстину(Завершилось, "Задание не должно ждать само себя"); + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Состояние); + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Результат.СостояниеВложенного); + юТест.ПроверитьИстину(Задание.Результат.ТекущееВРеестре, "Текущее задание не должно удаляться из реестра"); + +КонецПроцедуры + +Функция ЗапуститьДочернееИДождаться() Экспорт + + Дочернее = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗапуститьВложенноеИДождатьсяВсехЗадач"); + Возврат Дочернее.ОжидатьЗавершения(5000); + +КонецФункции + +Процедура ТестДолжен_ОжидатьЗавершенияЗадачИзЗаданияНеОжидаяРодителя() Экспорт + + // Родитель ждет дочернее задание, а оно - свои вложенные: родителя ждать нельзя + Родитель = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗапуститьДочернееИДождаться"); + Завершилось = Родитель.ОжидатьЗавершения(10000); + + юТест.ПроверитьИстину(Завершилось, "Родитель не завершился"); + юТест.ПроверитьИстину(Родитель.Результат, "Дочернее задание ждало родителя"); + +КонецПроцедуры + +Процедура ТестДолжен_ОжидатьЗавершенияЗадачИзСоседнихЗаданий() Экспорт + + Задания = Новый Массив; + Задания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗапуститьВложенноеИДождатьсяВсехЗадач")); + Задания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗапуститьВложенноеИДождатьсяВсехЗадач")); + + юТест.ПроверитьИстину(ФоновыеЗадания.ОжидатьВсе(Задания, 10000), "Соседние задания ждали друг друга"); + +КонецПроцедуры + +Процедура ТестДолжен_ЗавершитьАварийноЗаданиеСОшибкойВстроенногоОбъекта() Экспорт + + // Метод встроенного объекта бросает исключение .NET, а не исключение сценария + Путь = ОбъединитьПути(КаталогВременныхФайлов(), Строка(Новый УникальныйИдентификатор), "нет.txt"); + Задание = ФоновыеЗадания.Выполнить(Новый Файл(Путь), "Размер"); + Задание.ОжидатьЗавершения(5000); + + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.ЗавершеноАварийно, Задание.Состояние); + юТест.ПроверитьНеРавенство(Неопределено, Задание.ИнформацияОбОшибке); + +КонецПроцедуры + +Функция ЗаблокироватьСТаймаутом(Сигнал, Параметры) Экспорт + + Сигнал.Добавить(Истина); + Захвачена = Параметры.Блокировка.Заблокировать(Параметры.Таймаут); + Если Захвачена Тогда + Параметры.Блокировка.Разблокировать(); + КонецЕсли; + + Возврат Захвачена; + +КонецФункции + +Процедура ТестДолжен_ЗахватитьСвободнуюБлокировкуСТаймаутом() Экспорт + + Блокировка = Новый БлокировкаРесурса; + юТест.ПроверитьИстину(Блокировка.Заблокировать(100)); + Блокировка.Разблокировать(); + +КонецПроцедуры + +Процедура ТестДолжен_ВернутьЛожьПоТаймаутуБлокировки() Экспорт + + Блокировка = Новый БлокировкаРесурса; + Держатель = ЗапуститьСоСигналом(ЭтотОбъект, "ДержатьБлокировку", Блокировка); + + // В основном потоке ожидание без отмены + Начало = ТекущаяУниверсальнаяДатаВМиллисекундах(); + ЗахваченаВОсновномПотоке = Блокировка.Заблокировать(300); + Прошло = ТекущаяУниверсальнаяДатаВМиллисекундах() - Начало; + + // В фоновом задании ожидание с проверкой отмены + Ждущее = ЗапуститьСоСигналом(ЭтотОбъект, "ЗаблокироватьСТаймаутом", + Новый Структура("Блокировка, Таймаут", Блокировка, 300)); + Ждущее.ОжидатьЗавершения(5000); + + Держатель.Отменить(); + ПроверитьЧтоЗаданиеОтменено(Держатель); + + юТест.ПроверитьЛожь(ЗахваченаВОсновномПотоке); + юТест.ПроверитьБольшеИлиРавно(Прошло, 250, "Ожидание должно длиться не меньше таймаута"); + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Ждущее.Состояние); + юТест.ПроверитьРавенство(Ложь, Ждущее.Результат); + +КонецПроцедуры + +Процедура ТестДолжен_ЗахватитьБлокировкуДоИстеченияТаймаута() Экспорт + + Блокировка = Новый БлокировкаРесурса; + Держатель = ЗапуститьСоСигналом(ЭтотОбъект, "ДержатьБлокировку", Блокировка); + Ждущее = ЗапуститьСоСигналом(ЭтотОбъект, "ЗаблокироватьСТаймаутом", + Новый Структура("Блокировка, Таймаут", Блокировка, 10000)); + + Держатель.Отменить(); + + юТест.ПроверитьИстину(Ждущее.ОжидатьЗавершения(5000), "Ожидающее задание должно получить блокировку"); + юТест.ПроверитьРавенство(Истина, Ждущее.Результат); + +КонецПроцедуры + +Процедура ТестДолжен_ЗапретитьОтрицательныйТаймаутБлокировки() Экспорт + + Блокировка = Новый БлокировкаРесурса; + Попытка + Блокировка.Заблокировать(-1); + Исключение + Возврат; + КонецПопытки; + + ВызватьИсключение "Ожидали исключение на отрицательном таймауте"; + +КонецПроцедуры