前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >making Task<T> awaitable

making Task<T> awaitable

作者头像
阿新
发布2018-04-12 15:15:35
6520
发布2018-04-12 15:15:35
举报
文章被收录于专栏:c#开发者

Eduasync part 5: making Task<T> awaitable

In part 3 we looked at what the C# 5 compiler required for you to "await" something. The sample used a class which actually had an instance method called GetAwaiter, but I mentioned that it could also be an extension method.

In this post, we'll use that ability to make Task<T> awaitable - at which point we have everything we need to actually see some asynchronous behaviour. Just like the last part, the code here is pretty plain - but by the end of the post we'll have a full demonstration of asynchrony. I should make it clear that this isn't absolutely everything we'll want, but it's a good start.

TaskAwaiter<T>

As it happens, I'm using the same type name as the async CTP does for "something which can await a task" - TaskAwaiter. It's in a different namespace though, and we couldrename it with no ill-effects (unlike AsyncTaskMethodBuilder, for example). Indeed, in an old version of similar code I called this type Garçon - so GetAwaiter would return a waiter, so to speak. (Yes, you can use non-ASCII characters in identifiers in C#. I wouldn't really advise it though.)

All the task awaiter needs is a reference to the task that it's awaiting - it doesn't need to know anything about the async method which is waiting for it, for example. Task<T> provides everything we need: a property to find out whether it's completed, another to fetch the result, and the ContinueWith method to specify a continuation. The last part is particularly important - without that, we'd really have a hard time implementing this efficiently.

The extension method on Task<T> is trivial:

public static class TaskExtensions  {  public static TaskAwaiter<T> GetAwaiter<T>(this Task<T> task)      {  return new TaskAwaiter<T>(task);      }  }

The TaskAwaiter<T> type itself is slightly less so, but only slightly:

public struct TaskAwaiter<T>  {  private readonly Task<T> task;  internal TaskAwaiter(Task<T> task)      {  this.task = task;      }  public bool IsCompleted { get { return task.IsCompleted; } }  public void OnCompleted(Action action)      {          SynchronizationContext context = SynchronizationContext.Current;          TaskScheduler scheduler = context == null ? TaskScheduler.Current              : TaskScheduler.FromCurrentSynchronizationContext();          task.ContinueWith(ignored => action(), scheduler);      }  public T GetResult()      {  return task.Result;      }  }

IsCompleted is obviously trivial - Task<T> provides us exactly what we need to know. It's just worth noting that IsCompleted will return true if the task is cancelled, faulted or completed normally - it's not the same as checking for success. However, it represents exactly what we want to know here.

OnCompleted has two very small aspects of interest:

  • ContinueWith takes an Action<Task<T>> or an Action<Task>, not just an Action. That means we have to create a new delegate to wrap the original continuation. I can't currently think of any way round this with the current specification, but it's slightly annoying. If the compiler could work with an OnCompleted(Action<object>) method then we could pass that into Task<T>.ContinueWith due to contravariance of Action<T>. The compiler could generate an appropriate MoveNext(object) method which just called MoveNext() and stash an Action<object> field instead of an Action field... and do so only if the async method actually required it. I'll email the team with this as a suggestion - they've made other changes with performance in mind, so this is a possibility. Other alternatives:
    • In .NET 5, Task<T> could have ContinueWith overloads accepting Action as a continuation. That would be simpler from the language perspective, but the overload list would become pretty huge.
    • I would expect Task<T> to have a "real" GetAwaiter method in .NET 5 rather than the extension method; it could quite easily just return "this", possibly with some explicitly implemented IAwaiter<T> interface to avoid polluting the normal API. That could then handle the situation more natively.
  • We're using the current synchronization context if there is one to schedule the new task. This is the bit that lets continuations keep going on the UI thread for WPF and WinForms apps. If there isn't a synchronization context, we just use the current scheduler. For months this was incorrect in Eduasync; I was using TaskScheduler.Current in all cases. It's a subtle difference which has a huge effect on correctness; apologies for the previous inaccuracy. Even the current code is a lot cruder than it could be, but it should be better than it was...

GetResult looks and is utterly trivial - it works fine for success cases, but it doesn't do what we really want if the task has been faulted or cancelled. We'll improve it in a later part.

Let's see it in action!

Between this and the AsyncTaskMethodBuilder we wrote last time, we're ready to see an end-to-end asynchronous method demo. Here's the full code - it's not as trivial as it might be, as I've included some diagnostics so we can see what's going on:

internal class Program  {  private static readonly DateTimeOffset StartTime = DateTimeOffset.UtcNow;  private static void Main(string[] args)      {          Log("In Main, before SumAsync call");          Task<int> task = SumAsync();          Log("In Main, after SumAsync returned");  int result = task.Result;          Log("Final result: " + result);      }  private static async Task<int> SumAsync()      {          Task<int> task1 = Task.Factory.StartNew(() => { Thread.Sleep(500); return 10; });          Task<int> task2 = Task.Factory.StartNew(() => { Thread.Sleep(750); return 5; });          Log("In SumAsync, before awaits");  int value1 = await task1;  int value2 = await task2;          Log("In SumAsync, after awaits");  return value1 + value2;      }  private static void Log(string text)      {          Console.WriteLine("Thread={0}. Time={1}ms. Message={2}",                            Thread.CurrentThread.ManagedThreadId,                            (long)(DateTimeOffset.UtcNow - StartTime).TotalMilliseconds,                            text);      }  }

And here's the result of one run:

Thread=1. Time=12ms. Message=In Main, before SumAsync call  Thread=1. Time=51ms. Message=In SumAsync, before awaits  Thread=1. Time=55ms. Message=In Main, after SumAsync returned  Thread=4. Time=802ms. Message=In SumAsync, after awaits  Thread=1. Time=802ms. Message=Final result: 15

So what's going on?

  • We initially log before we even start the async method. We can see that the thread running Main has ID 1.
  • Within SumAsync, we start two tasks using Task.Factory.StartNew. Each task just has to sleep for a bit, then return a value. Everything's hard-coded.
  • We log before we await anything: this occurs still on thread 1, because async methods run synchronously at least as far as the first await.
  • We hit the first await, and because the first task hasn't completed yet, we register a continuation on it, and immediately return to Main.
  • We log that we're in Main, still in thread 1.
  • When the first await completes, a thread from the thread pool will execute the continuation. (This may well be the thread which executed the first task; I don't know the behaviour of the task scheduler used in console apps off the top of my head.) This will then hit the second await, which also won't have finished - so the first continuation completes, having registered a second continuation, this time on the second task. If we changed the Sleep calls within the tasks, we could observe this second await actually not needing to wait for anything.
  • When the second continuation fires, we log that fact. Two things to notice:
    • It's almost exactly 750ms after the earlier log messages. That proves that the two tasks has genuinely been executing in parallel.
    • It's on thread 4.
  • The final log statement occurs immediately after we return from the async method - thread 1 has been blocked on the task.Result property fetch, but when the async method completes, it unblocks and shows the result.

I think you'll agree that for the very small amount of code we've had to write, this is pretty nifty.

Conclusion

We've now implemented enough of the functionality which is usually in AsyncCtpLibrary.dll to investigate what the compiler's really doing for us. Next time I'll include a program showing one option for using the same types within hand-written code... and point out how nasty it is. Then for the next few parts, we'll look at what the C# 5 compiler does when we let it loose on code like the above... and show why I didn't just have "int value = await task1 + await task2;" in the sample program.

If you've skimmed through this post reasonably quickly, now would be a good time to go back and make sure you're really comfortable with where in this sample our AsyncTaskMethodBuilder is being used, and where TaskAwaiter is being used. We've got Task<T> as the core type at both boundaries, but that's slightly coincidental - the boundaries are still very different, and it's worth making sure you understand them before you try to wrap your head round the compiler-generated code.

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2013-01-05 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Eduasync part 5: making Task<T> awaitable
    • TaskAwaiter<T>
      • Let's see it in action!
        • Conclusion
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档