C# Core - Task.Wait (int Timeout) не ожидает ожидаемого - PullRequest
0 голосов
/ 04 мая 2020

Я пытаюсь реализовать механизм тайм-аута для вызовов WCF, который не поддерживает CancellationTokens или заранее заданные тайм-ауты. Для этого я создал небольшой проект и придумал эту структуру:

       var st = Stopwatch.StartNew();

       try
       {
           var responseTask = Task.Run(() =>
           {
               var task = new WaitingService.ServiceClient().GetDataAsync(60000);
               if (!task.Wait(1000))
               {
                   return null;
               }
               return task.Result;
           });
           await responseTask;
       }
       catch { }
       return Ok("Awaited for " + st.ElapsedMilliseconds + "ms. Supposed to be " + sleep);

Когда я запускаю этот код на своем локальном компьютере последовательно (по одному вызову за раз), результат очень ОЧЕНЬ близок до 1000, пропуская примерно от 10 до 50 мс, что на 100% приемлемо.
Но если я запускаю это одновременно, скажем, 5 запросов за раз, он начинает проскальзывать до 100 мс ... если Я запускаю это при 25 одновременных запросах. Я начинаю видеть смещение в секундах, а когда я бегу выше 35, смещение превышает 10 секунд (в этот момент я увеличил время сна до 60 с, потому что служба возвращалась до того, как фреймворк мог «заметить») что у него был таймаут)

Кто-нибудь может мне сказать, что происходит? почему этот «промах» происходит в такой степени? есть ли более надежная реализация того, чего я пытаюсь достичь?

Подробности:
Услуга очень проста:

public string GetData(int value)
{
    Console.WriteLine("Will sleep for " + value);
    Thread.Sleep(value);
    return string.Format("Slept for: {0}ms", value);
}

РЕДАКТИРОВАТЬ 1
Я также тестировал этот сценарий:

       var st = Stopwatch.StartNew();
       CancellationTokenSource src = new CancellationTokenSource(1000);

       try
       {
           var responseTask = Task.Run(() =>
           {
               var task = new WaitingService.ServiceClient().GetDataAsync(sleep);
               if (!task.Wait(1000,src.Token))
               {
                   return null;
               }
               task.Wait(src.Token);
               return task.Result;
           });
           await responseTask;
       }
       catch { }
       return Ok("Awaited for " + st.ElapsedMilliseconds + "ms. Supposed to be " + sleep);

Но на самом деле результаты были хуже ... скольжение усилилось ...

EDIT 2 :
следующая реализация дала НАМНОГО лучшие результаты: 50 одновременных запросов редко превышают 2 секунды!

var st = Stopwatch.StartNew();

try
{
    var responseTask = Task.Run(async () =>
    {
        var task = new WaitingService.ServiceClient().GetDataAsync(sleep);
        do
        {
            await Task.Delay(50);
        }while (task.Status == TaskStatus.Running || st.ElapsedMilliseconds < 1000);
        if (task.Status == TaskStatus.RanToCompletion)
        {
            return task.Result;
        }
        else { return null; }
    });
    await responseTask;
}
catch { }
return Ok("Awaited for " + st.ElapsedMilliseconds + "ms. Supposed to be " + sleep);

Ответы [ 2 ]

1 голос
/ 05 мая 2020

Как объясняли другие в комментариях, исходный фрагмент кода async блокировался с использованием Task.Wait и Task.Result. Это может вызвать проблемы , и этого не следует делать.

Использование Task.Run приводило к увеличению времени выполнения, которое вы видели, поскольку пул потоков истощался с увеличивающееся количество последовательных вызовов.

Если вы хотите запустить Task с тайм-аутом, без блокировки, вы можете использовать следующий метод:

public static async Task<(bool hasValue, TResult value)> WithTimeout<TResult>(Task<TResult> task, int msTimeout)
{
    using var timeoutCts = new CancellationTokenSource();

    var timeoutTask = Task.Delay(msTimeout, timeoutCts.Token);
    var completedTask = await Task.WhenAny(task, timeoutTask);

    if (completedTask == task)
    {
        timeoutCts.Cancel(); //Cancel timeoutTask
        return (true, await task); //Get result or propagate exception
    }

    //completedTask was our timeoutTask
    return (false, default);
}

Этот метод объединяет Task.WhenAny и Task.Delay, чтобы запустить задачу тайм-аута вместе с переданным аргументом задачи. Если переданная задача завершится до истечения времени ожидания, будет возвращен результат. Однако, если тайм-аут завершается первым, возвращается (hasValue: false, value: default).

Использование в вашем примере:

var task = new WaitingService.ServiceClient().GetDataAsync(sleep);
var result = await WithTimeout(task, 1000);

if (result.hasValue)
{
   //Do something with result.value;
}

Важно отметить, что, поскольку ваша задача не поддерживает отмену, она будет продолжена бежать. Если он не обрабатывается где-то еще, он может вызвать исключение, которое не было перехвачено.

Поскольку мы ожидаем выполнения задач через неблокирующий Task.WhenAny, вы не должны исчерпывать пул потоков, как вы видели в исходный пример.

В вашем последнем примере есть вероятность ненужной дополнительной задержки 0-50 мс перед продолжением работы с результатом. Этот метод вернется немедленно, когда задача завершится в течение периода ожидания.

1 голос
/ 04 мая 2020

Допустим, у вас есть 10 потоков в вашем процессе, и потоки планируются циклически, когда каждый поток получает только 2 мс времени выполнения. Предположим, что если все 10 потоков запускаются на 0-й мс и прошли 20 мс из StopWatch, каждый поток получает только 4 мс времени выполнения, а оставшиеся 16 мс каждый поток будет ждать своей очереди для выполнения. Таким образом, если вы блокируете определенный поток c на определенное количество миллисекунд с указанным значением времени ожидания c, это не означает, что выполнение потока будет завершено за это указанное время. Фактическое время, затрачиваемое потоком на завершение выполнения, включает время, в течение которого поток ожидал получения следующего цикла выполнения.

Таким образом, когда вы вызываете task.Wait(1000), поток будет заблокирован на 1000 мс времени его выполнения, но не истекшее время секундомера

...