7

I've got a WPF application which makes use of await and async methods extensively. There are several places where I call await Task.Delay(...); to insert pauses. But the trouble I'm running into is that while many of these pauses are fine to be a little bit off, there are some places where I absolutely need the pause to be precise. So in other words if I call await Task.Delay(2000); there are some places in my application where I need to guarantee that it's only going to pause for 2 seconds (and not 2.2+ seconds).

I believe the trouble comes from the fact that I do have so many different async methods, so that when I tell one of them to delay, there aren't enough threads left in the thread pool right when it's supposed to come back alive, which results, inadvertently, in longer delays than intended.

What are the C# "best practices" for threading when you have a business need for your delays to be as accurate as possible? Clearly it doesn't seem like async methods are enough on their own (even though they are nice to read). Should I manually create a thread with a higher priority and use Thread.Sleep? Do I up the number of threads in the thread pool? Do I use a BackgroundWorker?

soapergem
  • 9,263
  • 18
  • 96
  • 152
  • 2
    First thing to check is why your believe your ThreadPool is getting clogged up... Perhaps you're scheduling blocking workloads to run in the ThreadPool, in which case, I'd recommend isolating these cases and making sure they operate fully asynchronously. I've had problems with HttpWebRequest (ergo WebClient and HttpClient) because the DNS lookup on an async request is actually performed synchronously and ends up causing ThreadPool starvation. Could this type of problem be affecting you? – spender Jun 29 '15 at 22:23
  • Assuming your code does not block your delays are already "as accurate as possible" :) - Windows is not RT OS. Busy wait on high-pri thread is plausible option - http://stackoverflow.com/questions/9891879/c-sharp-timer-for-millisecond-waits. – Alexei Levenkov Jun 29 '15 at 22:23
  • 1
    Are these tasks running on the UI thread? – Scott Chamberlain Jun 29 '15 at 22:26
  • 2
    Besides potential `ThreadPool` [stuttering issue](http://joeduffyblog.com/2006/07/08/clr-thread-pool-injection-stuttering-problems/), your `await Task.Delay()` continuation gets (most likely) marshaled back to the WPF UI thread, via `DispatcherSynchronizationContext.Post`. The UI thread itself can be busy doing some work between its main message loop iterations. You have no direct control over *when* the continuation will actually gets executed. – noseratio Jun 30 '15 at 01:34
  • `There are several places where I call await Task.Delay(...); to insert pauses.` Why? Sounds like an XY problem to me. You should almost never have to use `Task.Delay` in production; and even in the one use case I can think of (retry backoff), it doesn't have to be accurate. – Stephen Cleary Jun 30 '15 at 12:04
  • @StephenCleary I can't get into much detail due to a non-disclosure agreement, but suffice it to say that I'm sending commands to proprietary hardware via a serial port, and it is crucial to insert pauses between certain commands, and those pauses need to be fairly accurate. – soapergem Jun 30 '15 at 13:27
  • @SoaperGEM: That's painful. I'd consider writing the actual communications software in C/C++ running as a service or dll, and control it from .NET. – Stephen Cleary Jun 30 '15 at 16:47

2 Answers2

7

You can't really make Task.Delay itself more accurate as it's based on an internal Threading.Timer which has a resolution of up to 15 ms and scheduling the callback to the thread pool takes its time.

If you really need to be accurate you need a dedicated thread. You can have it sleep for 2 seconds with Thread.Sleep and when it wakes up do what you need to do.

Since Thread.Sleep causes a context-switch where the thread goes out of the CPU an even more accurate option would be to do a "busy wait" (e.g. with a while loop). That will remove the cost of the context-switch back to the CPU which takes some time.

You should realize though that these options require too much resources and you should consider whether that's really necessary.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • A 15 ms margin of error is fine, but when it starts to be more like a 200+ ms margin of error that's where I run into problems. – soapergem Jun 29 '15 at 22:25
  • 1
    @SoaperGEM That's probably the cost of the `ThreadPool`'s scheduling, just as you suggested. – i3arnon Jun 29 '15 at 22:26
1
 public static async void ExecuteWithDelay( this Action action, int delay )
    {
      //await Task.Delay(delay);

      Action a = () => { new System.Threading.ManualResetEventSlim(false).Wait(delay); };
      await Task.Factory.StartNew(a);
      action?.Invoke ();
    }