C# Контроль времени выполнения Task и его отмена

148
21 апреля 2019, 21:00

получаю данный от Tcp сервера, если запрос длится более 1 сек, то завершаем Task ожидания получения ответа и выкидываем TimeoutException("...."). Также если прилетит CancellationToken, то сразу все отключаем и выкидываем OperationCanceledException("....").

В разных проектах замечал разные реализации такого функционала, но все они какие-то мутные, помогите разобраться

1. Самый частый вариант

public static T WithTimeout<T>(this Task<T> task, int time, CancellationToken ct)
{
    var isCompletedSuccessfully = task.Wait(time, ct);
    if (isCompletedSuccessfully)
    {
        return task.Result;
    }
    throw new TimeoutException("The function has taken longer than the maximum time allowed.");
} 
    //Но тут task мы не отключаем, тоесть будем плодить висячие таски.
    //task.Wait() синхронное ожидание заврешения, что тоже неверно.

2. Тоже в паре проектов используется.

    public static async Task<T> WithTimeout<T>(Task<T> task, int time, CancellationToken ct)
    {
        Task delayTask = Task.Delay(time, ct);
        Task firstToFinish = await Task.WhenAny(task, delayTask);
        if (firstToFinish == delayTask)
        {
            task.ContinueWith(HandleException, ct);  //к основной задаче прикрепили обработку иключений
            throw new TimeoutException();
        }
        return await task;
    }
    private static void HandleException<T>(Task<T> task)
    {
        if (task.Exception != null)
        {
            ; //чтото делаем с исключеним возникшим в основной задаче.
        }
    }
 //task также не отключаем а подписываемся на резульат выполнения и все таки ЖДЕМ.

А если просто использовать CancelAfter? Ну да из минусов постоянно нужно создать CancellationTokenSource перед вызовом метода и связывать им таски. Но вроде он работает правильно?

public static async Task<T> WithTimeout<T>(this Task<T> task, int time, CancellationTokenSource ctsTask)
{
    ctsTask.CancelAfter(time);
    try
    {
        return await task;
    }
    catch (OperationCanceledException ex)
    {
        throw new TimeoutException("The function has taken longer than the maximum time allowed.");
    }
}

    **ИСПОЛЬЗОВАНИЕ:**

public async Task<byte[]> TakeDataAsync(int nbytes, int timeOut, CancellationToken ct)
{
    byte[] bDataTemp = new byte[256];
    var ctsTimeout = new CancellationTokenSource();//токен сработает по таймауту в функции WithTimeout
    var cts = CancellationTokenSource.CreateLinkedTokenSource(ctsTimeout.Token, ct); // Объединенный токен, сработает от выставленного ctsTimeout.Token или от ct
    int nByteTake = await _terminalNetStream.ReadAsync(bDataTemp, 0, nbytes, cts.Token).WithTimeout(timeOut, ctsTimeout);
    if (nByteTake == nbytes)
    {
        var bData = new byte[nByteTake];
        Array.Copy(bDataTemp, bData, nByteTake);
        return bData;
    }
    return null;
}

Т.е. Связываем таски через CancellationTokenSource. и Просто отменяем задачу по времени. Чтобы отменить задачу по любому из токенов использую CancellationTokenSource.CreateLinkedTokenSource()

еще не понятно как отличать завершилась задача по времени или по ОТМЕНЕ, т.к. cts общий. И еще нужен самый быстрый вариант по перфомансу, т.к. используется клиент на RasberiPi под AspNetCore.

Нужно ли уничтожать CancellationTokenSource через Dispose после отработки функции?

Answer 1

Была точно такая же задача. Вот результаты моих изысканий.

  1. CancellationTokenSource при использовании CancelAfter лучше диспозить, ведь это отменит внутренний таймер и срабатывание возможных калбэков (которые были добавлены через Register()), когда это не нужно.
  2. Для отличия OperationCanceledException смотрят на состояние токенов. Если вылетел OperationCanceledException и клиентский токен отменен, значит отменил клиент, иначе что-то другое отменило (наш CancellationTokenSource)
  3. Вариант WithTimeout с CancelAfter() в вашем случае это просто враппер для создания нужного исключения. И поскольку он не может разобрать чья была отмена, то нужно перенести создание CancellationTokenSource внутрь WithTimeout и передавать клиентский токен для сравнения "кто отменил".
  4. Реализация WithTimeout c Task.Delay имхо неправильная, ведь забытая задача продолжает работать, а нужно бы ее отменить, поэтому метод сам по себе должен иметь и свой CancellationTokenSource в общем случае

Впрочем, самое важное то, что у вас специфическая задача.

Дело в том, что NetworkStream.ReadAsync игнорирует CancellationToken (но принимает. фича:) ) и если вы ожидаете, что при отмене он кинет исключение при срабатывании CancelAfter(), то этого не будет.

Поэтому при таймауте нужно закрыть сокет, чтобы ReadAsync() отвалился. При этом вылетит совсем не OperationCanceledException и приходится это детектить. Вот вам для примера псевдокод:

public async Task<...> RequestAsync(..., TimeSpan timeout, CancellationToken ct)
{
    using (var tcpClient = new TcpClient())
    {
        var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        try
        {
            cts.CancelAfter(timeout);
            try
            {
                using (ct.Register(tcpClient.Close))
                {
                    //...
                }
            }
            catch (ObjectDisposedException)
            {
                ct.ThrowIfCancellationRequested();
                throw;
            }
            catch (IOException ioException)
            {
                ct.ThrowIfCancellationRequested();
                throw new ...("error", ioException);
            }
            catch (SocketException socketException)
            {
                ct.ThrowIfCancellationRequested();
                throw new ...("error", socketException);
            }
        }
        catch (OperationCanceledException) when (cts.IsCancellationRequested)
        {
            //тут "наш" OperationCanceledException 
        }
    }
}

Также можно ловить все OperationCanceledException и смотреть на токены на случай если вдруг кто-то еще может бросить токен отмены (например, HttpClient так делает) если нужно разное поведение. Ну или сделать несколько catch..when

Остается вопрос: "а если я не хочу закрывать сокет при таймауте". На этот вопрос я ответа не знаю.

update вернее знаю вот такой вариант Но, насколько я понимаю, мы так получаем повисшую на ReadAsync таску и сокет в неопределенном состоянии, с которым непонятно что делать.

Вот короче вариант если воспользоваться WithCancellation из ссылки

  private async Task<...> MakeRequestAsync(..., TimeSpan timeout, CancellationToken ct)
  {
      using (var tcpClient = new TcpClient())
      {
          var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
          cts.CancelAfter(timeout);
          Task op = _stream.ReadAsync(..., cts.Token)
          try
          {
              await op.WithCancellation(cts.Token); 
          }
          catch (OperationCanceledException)
          {
              //у нас повис ReadAsync который выбросит исключение после закрытия сокета, а значит нужно их погасить
              if (!op.IsCompleted)
                  op.ContinueWith(t => /* handle eventual completion */);
              //при этом исключения IO/Socket*Exception не будут пойманы 
          }
      }
  }
READ ALSO
В C# windows form преобразование типа string в double и наоборот

В C# windows form преобразование типа string в double и наоборот

Как сделать что бы из textbox значения перевести в тип double и что бы читалась в числе и точка и запятая?И как вывести в label тип double?

180
Почему не получается десериализовать Json?

Почему не получается десериализовать Json?

Зашел в тупик с десериализацией Json, подскажите, почему?

143
BlockingCollection TryTake C#

BlockingCollection TryTake C#

Делаю так:

166
Проблема при изменении иерархии

Проблема при изменении иерархии

Есть такая иерархия классов: Window->baseMaker->someMaker

115