0

I'm trying to implement a timeout mechanism for WCF calls that don't support CancellationTokens or pre-defined timeouts. In order to do that, I created a small project and came up with this structure:

       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);

When I run this code on my local machine sequentially (1 call at a time) the output is very VERY close to 1000, missing by around 10 to 50ms, which is 100% acceptable.
But if I run this on a concurrent manner, let's say 5 request at-a-time, it starts to slip, to 100ms... if I run this on 25 concurrent requests I start to see the slip in seconds and when I run above 35 the slip is superior to 10 seconds (at which point I increased the sleep to 60s because the service was returning before the framework could "notice" that it had timeout)

Can anyone tell me what's going on? why is this "slip" happening at such a degree? is there a more reliable implementation for what I'm trying to achieve?

Details:
The service is very simple:

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

EDIT 1
I also tested this scenario:

       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);

But I actually got worse results... the slip intensified...

EDIT 2:
the following implementation got FAR better results, with 50 concurrent request rarely exceeding 2 seconds!

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);
Leonardo
  • 10,737
  • 10
  • 62
  • 155
  • 5
    Do not block in async methods – Pavel Anikhouski May 04 '20 at 19:57
  • @Oliver you are suggesting to properly implement "C# task with timeout" (https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout), totally not what OP is doing - presumably they are trying to consume as many threads as possible with `Run` and `Wait`... Not really sure why so - some clarification would be nice. – Alexei Levenkov May 04 '20 at 20:00
  • @AlexeiLevenkov I already deleted that hasty comment after I realised the same. – Oliver May 04 '20 at 20:00
  • @Oliver so now we wait till OP comes back to explain why they decided to go that route... – Alexei Levenkov May 04 '20 at 20:02
  • The reason you are seeing the huge difference between the expected and actual values is because `Stopwatch.ElapsedMilliSeconds` will also include the time the particular thread had been waiting for its execution. The more threads you spawn, the more waiting for an individual thread and the more difference you would get. When you do `task.Wait(1000)` the wait is guaranteed to end at 1000th ms of the thread execution time. – zafar May 04 '20 at 20:11
  • @PavelAnikhouski the block is not in the async method, the lambda expression says that it is a void sync method – zafar May 04 '20 at 20:17
  • @Oliver what route you mean? I think my goal here is quite direct: wait a call for a maximum of 1 second. If that request is not back, ignore it and move along – Leonardo May 04 '20 at 20:22
  • 1
    Try configuring the [`ThreadPool`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.threadpool) with `ThreadPool.SetMinThreads(100, 10)` at the start of the program, to see if it makes any difference. – Theodor Zoulias May 04 '20 at 20:37
  • @PavelAnikhouski do you have a workaround to avoid an endless-waiting for the wcf call that could never return? – Leonardo May 04 '20 at 20:40
  • @AlexeiLevenkov seems I was on the right track anyway... – Oliver May 05 '20 at 08:25
  • @Leonardo - not sure what you mean about route? I've posted an answer based on the additional information. – Oliver May 05 '20 at 13:43

2 Answers2

1

Say suppose you have 10 threads in your process, and the threads are scheduled in round robin fashion, where each thread is getting only 2ms of execution time. Suppose if all the 10 threads are launched at the 0th ms and 20ms of StopWatch has elapsed, each thread is getting only 4ms of execution time, the rest of 16 ms, each thread would be waiting for its turn to get executed. So if you are blocking a specific thread for a certain amount of ms with the specific timeout value, it doesn't mean that the thread execution would be completed in that specified time. The actual time taken by the thread to complete the execution, includes the time the thread has been waiting to acquire the next execution cycle.

So when you call task.Wait(1000) the thread will be blocked for 1000ms of its execution time, but not the stop watch elapsed time

zafar
  • 1,965
  • 1
  • 15
  • 14
  • given that the time is spent on awaiting IO (and not a loop) the switching should happen a lot faster, allowing for a very precise measurement... shouldn't it? but OK, I'll test that theory – Leonardo May 04 '20 at 21:02
  • Your process threads are not the only ones needed to be scheduled. There are other process and services with multiple threads needed to be executed by OS. So the more resources the OS has, the less difference in time you would see – zafar May 04 '20 at 21:11
  • Do you have the code where you are launching multiple requests? In your code I see only one task getting executed – zafar May 04 '20 at 23:32
1

As others have explained in comments, the original async code snippet was blocking with the the usage of Task.Wait and Task.Result. This can cause issues and shouldn't be done.

Using Task.Run was causing the increased execution time that you were seeing as the threadpool was becoming exhausted with the increasing number of sequential calls.

If you would like to run a Task with a timeout, without blocking, you could use the following method:

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);
}

This method combines Task.WhenAny and Task.Delay to run a timeout task alongside the task argument passed in. If the task passed in completes before the timeout, the result will be returned. However if the timeout completes first, a (hasValue: false, value: default) is returned.

Usage with your example:

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

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

It's important to note that as your task does not support cancellation, it will continue to run. Unless it's handled elsewhere, it could throw an exception that is not caught.

Because we're awaiting the tasks through the non-blocking Task.WhenAny, you shouldn't be exhausting the threadpool as you were seeing in the original example.

In your last example, there is potential for an unnecessary extra 0-50ms of delay before continuing with a result. This method will return immediately when a task completes within the timeout period.

Oliver
  • 8,794
  • 2
  • 40
  • 60
  • On preliminary production-like tests this performed wonderfully. On local machine the slip was still there, a bit worse actually, but performed much better than any of my previous tries on server-platforms – Leonardo May 06 '20 at 14:38
  • 1
    @Leonardo could it be something within the `new WaitingService.ServiceClient().GetDataAsync(sleep);` call that's blocking threads locally? The WCF service maybe running within the same app domain? – Oliver May 06 '20 at 14:47
  • 1
    This worked very well on our live production scenario so im accepting it as answer – Leonardo May 07 '20 at 16:40