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