博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
making Task<T> awaitable
阅读量:5871 次
发布时间:2019-06-19

本文共 8885 字,大约阅读时间需要 29 分钟。

Eduasync part 5: making Task<T> awaitable

In  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  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  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.

转载于:https://www.cnblogs.com/neozhu/archive/2013/01/05/2846590.html

你可能感兴趣的文章
Linux运维人员共用root帐户权限审计
查看>>
IT人怎能忘记这些开源?
查看>>
超详细Centos6.5文本模式安装步骤
查看>>
安装配置rabbitmq
查看>>
java-第十章-类和对象-创建管理员对象
查看>>
eigrp debug命令详解
查看>>
MySQL-Proxy实现MySQL读写分离
查看>>
安装nginx并搭建nginx图片服务器
查看>>
扩展JS格式化(Format)功能及评论树
查看>>
NFS配置文件
查看>>
Oracle 10g 完全卸载(windows平台和linux平台)
查看>>
shell 整理(36)===写斐波那契数列
查看>>
Python 日期和时间
查看>>
BMC之ipmitool 命令收集
查看>>
Go语言之Map
查看>>
数据库日志路径--数据库清理垃圾日志路径
查看>>
亚信安全首推MSP创新型合作伙伴业务模式 助力企业畅享云端快捷服务
查看>>
highcharts图表组件常见问题:highcharts图表组件错误集合分析大放送
查看>>
脚本:格式化的V$SQL_SHARED_CURSOR报告
查看>>
测试keepalived备备模式
查看>>