As for extensibility, if you can wrap a Task object around an operation that completes in the future, you can use the await operator to await that operation. Having a single type (Task) to represent all kinds of asynchronous operations is phenomenally useful because it allows you to implement combinators (like Task’s WhenAll and WhenAny methods) and other helpful operations. Later in this
chapter, I demonstrate doing this by wrapping a CancellationToken with a Task so I can await an asynchronous operation while also exposing timeout and cancellation.
I’d also like to share with you another example. The following is my TaskLogger class, which you can use to show you asynchronous operations that haven’t yet completed. This is very useful in
debugging scenarios especially when your application appears hung due to a bad request or a non- responding server.
public static class TaskLogger {
public enum TaskLogLevel { None, Pending }
public static TaskLogLevel LogLevel { get; set; }
public sealed class TaskLogEntry {
public Task Task { get; internal set; }
public String Tag { get; internal set; } public DateTime LogTime { get; internal set; }
public String CallerMemberName { get; internal set; } public String CallerFilePath { get; internal set; } public Int32 CallerLineNumber { get; internal set; } public override string ToString() {
// Ask the logger which tasks have not yet completed and sort
// them in order from the one that’s been waiting the longest
foreach (var op in TaskLogger.GetLogEntries().OrderBy(tle => tle.LogTime)) Console.WriteLine(op);
}
When I build and run this code, I get the following output.
LogTime=7/16/2012 6:44:31 AM, Tag=6s op, Member=Go, File=C:\CLR via C#\Code\Ch281IOOps.cs(332) LogTime=7/16/2012 6:44:31 AM, Tag=5s op, Member=Go, File=C:\CLR via C#\Code\Ch281IOOps.cs(331)
In addition to all the flexibility you have with using Task, async functions have another extensibil- ity point: the compiler calls GetAwaiter on whatever operand is used with await. So, the operand doesn’t have to be a Task object at all; it can be of any type as long as it has a GetAwaiter method available to call. Here is an example of my own awaiter that is the glue between an async method’s state machine and an event being raised.
public sealed class EventAwaiter<TEventArgs> : INotifyCompletion {
private ConcurrentQueue<TEventArgs> m_events = new ConcurrentQueue<TEventArgs>(); private Action m_continuation;
#region Members invoked by the state machine
// The state machine will call this first to get our awaiter; we return ourself public EventAwaiter<TEventArgs> GetAwaiter() { return this; }
// Tell state machine if any events have happened yet
public Boolean IsCompleted { get { return m_events.Count > 0; } }
// The state machine tells us what method to invoke later; we save it public void OnCompleted(Action continuation) {
Volatile.Write(ref m_continuation, continuation);
}
// The state machine queries the result; this is the await operator's result public TEventArgs GetResult() {
// Potentially invoked by multiple threads simultaneously when each raises the event
public void EventRaised(Object sender, TEventArgs eventArgs) { m_events.Enqueue(eventArgs); // Save EventArgs to return it from GetResult/await
// If there is a pending continuation, this thread takes it
Action continuation = Interlocked.Exchange(ref m_continuation, null); if (continuation != null) continuation(); // Resume the state machine
}
}
And here is a method that uses my EventAwaiter class to return from an await operator when- ever an event is raised. In this case, the state machine continues whenever any thread in the App- Domain throws an exception.
private static async void ShowExceptions() {
var eventAwaiter = new EventAwaiter<FirstChanceExceptionEventArgs>(); AppDomain.CurrentDomain.FirstChanceException += eventAwaiter.EventRaised;