Везде написано, что работа с задачами- это кооперативный процесс, т.е задача должна сама корректно завершится при первой просьбе из внешнего кода.
Но, что делать если кто-то подводит?
Например, я делаю Cancel
на токене и даю на завершение некоторое время, но задача не завершается, а код должен двигаться дальше. Оставлять висеть задачу?
Читал, что есть Thread.Abort
, но его не рекомендуют использовать.
Ну вот вам пример реализации. Сразу предупреждаю, кода будет много.
Возьмём в качестве основы вот такую ненадёжную функцию:
class EvilComputation
{
static Random random = new Random();
public static async Task<double> Compute(
int numberOfSeconds, double x, CancellationToken ct)
{
bool wellBehaved = random.Next(2) == 0;
var y = x * x;
var delay = TimeSpan.FromSeconds(numberOfSeconds);
await Task.Delay(delay, wellBehaved ? ct : CancellationToken.None);
return y;
}
}
Вы видим, что функция плохая: она может в зависимости от случайных условий не реагировать на отмену.
Что делать в этом случае? Вынесем функцию в отдельный процесс. Этот процесс можно будет убить без особого вреда для исходного процесса.
Для того, чтобы вызвать функцию в другом процессе, нужно передать данные о вызове функции туда. Для связи используем, например, анонимные пайпы (можно использовать по сути что угодно). Я основываю код на этом примере: How to: Use Anonymous Pipes for Local Interprocess Communication.
Для передачи данных будем использовать стандартное бинарное форматирование, раз уж мы не пошли через WCF. Нам нужны DTO-объекты, которые будут перебрасываться между процессами. Их нужно использовать в двух процессах — главном и вспомогательном (назовём его плагином), поэтому для DTO-типов понадобится отдельная сборка.
Заводим сборку OutProcCommonData
, кладём в неё следующие классы:
namespace OutProcCommonData
{
[Serializable]
public class Command // общий класс-предок для посылаемой команды
{
}
[Serializable]
public class Evaluate : Command // команда на вычисление
{
public int NumberOfSecondsToProcess;
public double X;
}
[Serializable]
public class Cancel : Command // команда на отмену
{
}
}
Далее, возвращаемый результат:
namespace OutProcCommonData
{
[Serializable]
public class Response // общий класс-предок для возвращаемого результата
{
}
[Serializable]
public class Result : Response // готовый результат вычислений
{
public double Y;
}
[Serializable]
public class Error : Response // ошибка с текстом
{
public string Text;
}
[Serializable]
public class Cancelled : Response // подтверждение отмены
{
}
}
Далее, наш плагин. Это отдельное консольное приложение (хотя, если мы не хотим видеть консоль и отладочный вывод, можно сделать его неконсольным).
Протокол общения таков. Главная программа посылает Evaluate
, а после него, возможно, Cancel
. Плагин возвращает Result
в случае успешного вычисления, Cancelled
в случае полученного сигнала отмены и успешно отменённого вычисления, и Error
в случае ошибки (например, нарушения протокола коммуникации).
Вот обвязочный код:
class Plugin
{
static int Main(string[] args)
{
// нам должны быть переданы два аргумента: хендл входящего и исходящего пайпов
if (args.Length != 2)
{
Console.Error.WriteLine("Shouldn't be started directly");
return 1;
}
return new Plugin().Run(args[0], args[1]).Result;
}
BinaryFormatter serializer = new BinaryFormatter(); // для сериализации
async Task<int> Run(string hIn, string hOut)
{
Console.WriteLine("[Plugin] Running");
// открывем переданные пайпы
using (var inStream = new AnonymousPipeClientStream(PipeDirection.In, hIn))
using (var outStream = new AnonymousPipeClientStream(PipeDirection.Out, hOut))
{
try
{
var cts = new CancellationTokenSource(); // токен для отмены
Console.WriteLine("[Plugin] Reading args");
// пытаемся десериализовать аргументы
var args = SafeGet<OutProcCommonData.Evaluate>(inStream);
if (args == null)
{
Console.WriteLine("[Plugin] Didn't get args");
// отправляем ошибку, если не удалось
serializer.Serialize(
outStream,
new OutProcCommonData.Error() { Text = "Unrecognized input" });
// и выходим
return 3;
}
Console.WriteLine("[Plugin] Got args, start compute and waiting cancel");
// запускаем вычисление
var computeTask =
EvilComputation.Compute(
args.NumberOfSecondsToProcess,
args.X,
cts.Token);
// параллельно запускаем чтение возможной отмены
var waitForCancelTask = Task.Run(() =>
(OutProcCommonData.Cancel)serializer.Deserialize(inStream));
// дожидаемся одного из двух
var winner = await Task.WhenAny(computeTask, waitForCancelTask);
// если первой пришла отмена...
if (winner == waitForCancelTask)
{
Console.WriteLine("[Plugin] Got cancel, cancelling computation");
// просим вычисление завершиться
cts.Cancel();
}
// окончания вычисления всё равно нужно дождаться
Console.WriteLine("[Plugin] Awaiting computation");
// если вычисление отменится, здесь будет исключение
var result = await computeTask;
Console.WriteLine("[Plugin] Sending back result");
// отсылаем результат в пайп
serializer.Serialize(
outStream,
new OutProcCommonData.Result() { Y = result });
// нормальный выход
return 0;
}
catch (OperationCanceledException)
{
// мы успешно отменили задание, рапортуем
Console.WriteLine("[Plugin] Sending cancellation");
serializer.Serialize(
outStream,
new OutProcCommonData.Cancelled());
return 2;
}
catch (Exception ex)
{
// возникла непредвиденная ошибка, рапортуем
Console.WriteLine($"[Plugin] Sending error {ex.Message}");
serializer.Serialize(
outStream,
new OutProcCommonData.Error() { Text = ex.Message });
return 3;
}
}
}
// ну и вспомогательная функция, которая пытается читать данные из пайпа
T SafeGet<T>(Stream s) where T : class
{
try
{
return (T)serializer.Deserialize(s);
}
catch
{
return null;
}
}
}
Я не отлавливаю ошибки при записи в пайп, добавьте сами по вкусу.
Теперь, главная программа. Она будет у нас отдельно от плагина (то есть, у нас получаются три сборки).
class Program
{
static void Main(string[] args) => new Program().Run().Wait();
async Task Run()
{
var cts = new CancellationTokenSource();
try
{
var y = await ComputeOutProc(2, cts.Token);
Console.WriteLine($"[Main] Result: {y}");
}
catch (TimeoutException)
{
Console.WriteLine("[Main] Timed out");
}
catch (OperationCanceledException)
{
Console.WriteLine("[Main] Cancelled");
}
}
const int SecondsToSend = 3;
const int TimeoutSeconds = 5;
const int CancelSeconds = 2;
BinaryFormatter serializer = new BinaryFormatter();
async Task<double> ComputeOutProc(double x, CancellationToken ct)
{
Process plugin = null;
bool pluginStarted = false;
try
{
// создаём исходящий и входящий пайпы
using (var commandStream = new AnonymousPipeServerStream(
PipeDirection.Out, HandleInheritability.Inheritable))
using (var responseStream = new AnonymousPipeServerStream(
PipeDirection.In, HandleInheritability.Inheritable))
{
Console.WriteLine("[Main] Starting plugin");
plugin = new Process()
{
StartInfo =
{
FileName = "OutProcPlugin.exe",
Arguments = commandStream.GetClientHandleAsString() + " " +
responseStream.GetClientHandleAsString(),
UseShellExecute = false
}
};
// запускаем плагин с параметрами
plugin.Start();
pluginStarted = true;
Console.WriteLine("[Main] Started plugin");
commandStream.DisposeLocalCopyOfClientHandle();
responseStream.DisposeLocalCopyOfClientHandle();
void Send(Command c)
{
serializer.Serialize(commandStream, c);
commandStream.Flush();
}
try
{
// отсылаем плагину команду на вычисление
Console.WriteLine("[Main] Sending evaluate request");
Send(new OutProcCommonData.Evaluate()
{
NumberOfSecondsToProcess = SecondsToSend,
X = x
});
Task<Response> responseTask;
bool readyInTime;
bool cancellationSent = false;
// внутри этого блока при отмене будем отсылать команду плагину
using (ct.Register(() =>
{
Send(new OutProcCommonData.Cancel());
Console.WriteLine("[Main] Requested cancellation");
cancellationSent = true;
}))
{
Console.WriteLine("[Main] Starting getting response");
// ожидаем получение ответа
responseTask = Task.Run(() =>
(Response)serializer.Deserialize(responseStream));
// или таймаута
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(TimeoutSeconds));
var winner = await Task.WhenAny(responseTask, timeoutTask);
readyInTime = winner == responseTask;
}
// если наступил таймаут, просим процесс вежливо завершить вычисления
if (!readyInTime)
{
if (!cancellationSent)
{
Console.WriteLine("[Main] Not ready in time, sending cancel");
Send(new OutProcCommonData.Cancel());
}
else
{
Console.WriteLine("[Main] Not ready in time, cancel sent");
}
// и ждём ещё немного, ну или прихода ответа
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(CancelSeconds));
await Task.WhenAny(responseTask, timeoutTask);
}
// если до сих пор ничего не пришло, плагин завис, убиваем его
if (!responseTask.IsCompleted)
{
Console.WriteLine("[Main] No response, killing plugin");
plugin.Kill(); // это завершит ожидание с исключением, по идее
// в ранних версиях .NET нужно было бы поймать
// это исключение
// и уходим с исключением-таймаутом
ct.ThrowIfCancellationRequested();
throw new TimeoutException();
}
// здесь мы уверены, что ожидание завершилось
Console.WriteLine("[Main] Obtaining response");
var response = await responseTask; // тут может быть брошено исключение
// если была затребована отмена, выходим
ct.ThrowIfCancellationRequested();
// проверяем тип результата:
switch (response)
{
case Result r:
// нормальный результат, возвращаем его
Console.WriteLine("[Main] Got result, returning");
return r.Y;
case Cancelled _:
// отмена не по ct = таймаут
Console.WriteLine("[Main] Got cancellation");
throw new TimeoutException();
case Error err:
// пришла ошибка, бросаем исключение
// лучше, конечно, определить собственный тип здесь
Console.WriteLine("[Main] Got error");
throw new Exception(err.Text);
default:
// сюда мы вообще не должны попасть, если плагин работает нормально
Console.WriteLine("[Main] Unexpected error");
throw new Exception("Unexpected response type");
}
}
catch (IOException e)
{
Console.WriteLine("[Main] IO error occured");
throw new Exception("IO Error", e);
}
}
}
finally
{
if (pluginStarted)
{
plugin.WaitForExit();
plugin.Close();
}
}
}
}
Результат пробега:
[Main] Starting plugin
[Main] Started plugin
[Main] Sending evaluate request
[Main] Starting getting response
[Plugin] Running
[Plugin] Reading args
[Plugin] Got args, start compute and waiting cancel
[Plugin] Awaiting computation
[Plugin] Sending back result
[Main] Obtaining response
[Main] Got result, returning
[Main] Result: 4
Если поменять константу SecondsToSend
на 10, чтобы был таймаут, получаем такой результат двух пробегов:
Для штатного завершения:
[Main] Starting plugin
[Main] Started plugin
[Main] Sending evaluate request
[Main] Starting getting response
[Plugin] Running
[Plugin] Reading args
[Plugin] Got args, start compute and waiting cancel
[Main] Not ready in time, sending cancel
[Plugin] Got cancel, cancelling computation
[Plugin] Awaiting computation
[Plugin] Sending cancellation
[Main] Obtaining response
[Main] Got cancellation
[Main] Timed out
Для принудительного завершения:
[Main] Starting plugin
[Main] Started plugin
[Main] Sending evaluate request
[Main] Starting getting response
[Plugin] Running
[Plugin] Reading args
[Plugin] Got args, start compute and waiting cancel
[Main] Not ready in time, sending cancel
[Plugin] Got cancel, cancelling computation
[Plugin] Awaiting computation
[Main] No response, killing plugin
[Main] Timed out
Если добавить перед
var y = await ComputeOutProc(2, cts.Token);
преждевременную отмену:
cts.CancelAfter(TimeSpan.FromSeconds(1));
получим такой результат: для штатного завершения
[Main] Starting plugin
[Main] Started plugin
[Main] Sending evaluate request
[Main] Starting getting response
[Plugin] Running
[Plugin] Reading args
[Plugin] Got args, start compute and waiting cancel
[Main] Requested cancellation
[Plugin] Got cancel, cancelling computation
[Plugin] Awaiting computation
[Plugin] Sending cancellation
[Main] Obtaining response
[Main] Cancelled
и для принудительного завершения
[Main] Starting plugin
[Main] Started plugin
[Main] Sending evaluate request
[Main] Starting getting response
[Plugin] Running
[Plugin] Reading args
[Plugin] Got args, start compute and waiting cancel
[Main] Requested cancellation
[Plugin] Got cancel, cancelling computation
[Plugin] Awaiting computation
[Main] Not ready in time, cancel sent
[Main] No response, killing plugin
[Main] Cancelled
Наверняка кое-где недостаточно контролируются ошибки, так что проверяйте, не нужно ли ловить какие-то ещё исключения.
Виртуальный выделенный сервер (VDS) становится отличным выбором
Есть ли панель, в которую можно было бы запихнуть Control'ы, которые не смогут выходить за пределы этой панели, Margin которых будет отсчитываться...
Прошу прощения если повторяюсь, но что то не могу найти ответаесть таблица datagridview заполняется так :