I have a blackbox object that exposes a method to kick of an async operation, and an event fires when the operation is complete. I have wrapped that into an Task<OpResult> BlackBoxOperationAysnc()
method using TaskCompletionSource - that works well.
However, in that async wrapper I'd like to manage completing the async call with a timeout error if the event is not received after a given timeout. Currently I manage it with a timer as:
public Task<OpResult> BlackBoxOperationAysnc() { var tcs = new TaskCompletionSource<TestResult>(); const int timeoutMs = 20000; Timer timer = new Timer(_ => tcs.TrySetResult(OpResult.Timeout), null, timeoutMs, Timeout.Infinite); EventHandler<EndOpEventArgs> eventHandler = (sender, args) => { ... tcs.TrySetResult(OpResult.BlarBlar); } blackBox.EndAsyncOpEvent += eventHandler; blackBox.StartAsyncOp(); return tcs.Task; }
Is that the only way to manage a timeout? Is there someway without setting up my own timer - I couldn't see anything timeout built into TaskCompletionSource?
Aside from void, which has specific use cases, Task is the primary return type used with async methods, along with the lesser used ValueTask. It might be surprising, though, to learn that the similarly named TaskCompletionSource is not an acceptable return type for async methods.
When working with async / await in C# you end up using the Task class quite a bit. Aside from void, which has specific use cases, Task is the primary return type used with async methods, along with the lesser used ValueTask.
… when used with async/await. TaskCompletionSource<T>class is a very useful facility if you want to control the lifetime of a task manually. Here is a canonical example when TaskCompletionSourceis used for converting the event-based asynchronous code to the Task-based pattern:
As you can see, the main thread waited until tcs.SetResult (true) was called; this triggered completion of the TaskCompletionSource’s underlying task (which the main thread was awaiting), and allowed the main thread to resume. Aside from SetResult (), TaskCompletionSource offers methods to cancel a task or fault it with an exception.
You could use CancellationTokenSource with timeout. Use it together with your TaskCompletionSource
like this.
E.g.:
public Task<OpResult> BlackBoxOperationAysnc() { var tcs = new TaskCompletionSource<TestResult>(); const int timeoutMs = 20000; var ct = new CancellationTokenSource(timeoutMs); ct.Token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false); EventHandler<EndOpEventArgs> eventHandler = (sender, args) => { ... tcs.TrySetResult(OpResult.BlarBlar); } blackBox.EndAsyncOpEvent += eventHandler; blackBox.StartAsyncOp(); return tcs.Task; }
Updated, here's a complete functional example:
using System; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; namespace ConsoleApplication { public class Program { // .NET 4.5/C# 5.0: convert EAP pattern into TAP pattern with timeout public async Task<AsyncCompletedEventArgs> BlackBoxOperationAsync( object state, CancellationToken token, int timeout = Timeout.Infinite) { var tcs = new TaskCompletionSource<AsyncCompletedEventArgs>(); using (var cts = CancellationTokenSource.CreateLinkedTokenSource(token)) { // prepare the timeout if (timeout != Timeout.Infinite) { cts.CancelAfter(timeout); } // handle completion AsyncCompletedEventHandler handler = (sender, args) => { if (args.Cancelled) tcs.TrySetCanceled(); else if (args.Error != null) tcs.SetException(args.Error); else tcs.SetResult(args); }; this.BlackBoxOperationCompleted += handler; try { using (cts.Token.Register(() => tcs.SetCanceled(), useSynchronizationContext: false)) { this.StartBlackBoxOperation(null); return await tcs.Task.ConfigureAwait(continueOnCapturedContext: false); } } finally { this.BlackBoxOperationCompleted -= handler; } } } // emulate async operation AsyncCompletedEventHandler BlackBoxOperationCompleted = delegate { }; void StartBlackBoxOperation(object state) { ThreadPool.QueueUserWorkItem(s => { Thread.Sleep(1000); this.BlackBoxOperationCompleted(this, new AsyncCompletedEventArgs(error: null, cancelled: false, userState: state)); }, state); } // test static void Main() { try { new Program().BlackBoxOperationAsync(null, CancellationToken.None, 1200).Wait(); Console.WriteLine("Completed."); new Program().BlackBoxOperationAsync(null, CancellationToken.None, 900).Wait(); } catch (Exception ex) { while (ex is AggregateException) ex = ex.InnerException; Console.WriteLine(ex.Message); } Console.ReadLine(); } } }
A .NET 4.0/C# 4.0 vesion can be found here, it takes advantage of the compiler-generated IEnumerator
state machine.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With