Страницы

Поиск по вопросам

понедельник, 16 декабря 2019 г.

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

#c_sharp #async_await


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

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

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

public static T WithTimeout(this Task 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 WithTimeout(Task 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(Task task)
    {
        if (task.Exception != null)
        {
            ; //чтото делаем с исключеним возникшим в основной задаче.
        }
    }

 //task также не отключаем а подписываемся на резульат выполнения и все таки ЖДЕМ.


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

public static async Task WithTimeout(this Task 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 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 после отработки функции?
    


Ответы

Ответ 1



Была точно такая же задача. Вот результаты моих изысканий. CancellationTokenSource при использовании CancelAfter лучше диспозить, ведь это отменит внутренний таймер и срабатывание возможных калбэков (которые были добавлены через Register()), когда это не нужно. Для отличия OperationCanceledException смотрят на состояние токенов. Если вылетел OperationCanceledException и клиентский токен отменен, значит отменил клиент, иначе что-то другое отменило (наш CancellationTokenSource) Вариант WithTimeout с CancelAfter() в вашем случае это просто враппер для создания нужного исключения. И поскольку он не может разобрать чья была отмена, то нужно перенести создание CancellationTokenSource внутрь WithTimeout и передавать клиентский токен для сравнения "кто отменил". Реализация 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 не будут пойманы } } }

Комментариев нет:

Отправить комментарий