使用async/await比使用task.Start()更好吗?为什么?

4

比较以下两种方法:

static async Task<int> DownloadAsync(string url)
{
    var client = new WebClient();
    var awaitable = client.DownloadDataTaskAsync(url);
    byte[] data = await awaitable;
    return data.Length;
}

用法:Task<int> task = DownloadAsync("http://stackoverflow.com");

这行代码是关于IT技术的。它表示创建一个名为“task”的异步任务,该任务会下载来自指定URL的数据并返回一个整数值。

如果您需要使用此代码,请确保提供正确的URL地址。

static Task<int> Download(string url)
{
    var client = new WebClient();
    var task = client.DownloadDataTaskAsync(url);
    byte[] data = task.Result;
    return Task.FromResult(data.Length);
}

使用方法:

Task task = new Task(() => Download("http://stackoverflow.com"));
task.Start();

据我所知,这两种方法都是异步运行的。我的问题是:
这两种方法之间有什么行为上的区别吗?
除了它是一种好的模式外,为什么我们更喜欢使用async-await?

1
请确保在不需要捕获同步上下文的方法中使用.ConfigureAwait(false),您在DownloadAsync中的所有等待都可以使用它。请参见此处以了解原因。 - Lukazoid
3个回答

7
你发表的两种方法完全不同。 DownloadAsync是一个真正的异步方法。这意味着在数据下载时,没有线程被阻塞在该异步操作上。 Download通过调用Task.Result在同步情况下阻塞了调用线程。我在我的博客上解释了为什么Result不应与异步Task一起使用:在一般情况下,它可能会导致死锁。但是让我们假设没有死锁。然后你从TPL任务中调用它,因此它会阻塞任务线程(很可能是线程池线程)。在数据下载时,该任务线程被阻塞在该异步操作上。
因此,DownloadAsync更有效率。

DownloadAsync 案例中没有工作线程。Result 的死锁问题是一个死锁问题,而不是一个阻塞问题。如果您还没有阅读过我的关于 async 的介绍文章,我建议您阅读一下。 - Stephen Cleary
阅读和理解是两回事 - 哎呀 ;) - Gerard
DownloadAsync怎么能够是“真正的异步方法”,同时又“没有工作线程”呢?我的意思是,等待部分明显是在工作线程上执行的,并且在完成后在该工作线程上返回到该方法中(通常情况下)。 - Gerard
1
@Gerard:不,"await部分"并没有在工作线程上执行。在高层次(稍微简化一下),发生的情况是这样的:DownloadDataTaskAsync只是向TCP/IP堆栈注册了一个回调函数。因此,没有线程一直等待那个数据包。当数据包到达时(从网络卡中断开始),DownloadDataTaskAsync完成其Task,进而安排其余的DownloadData执行。重点是,在发送请求到接收回复的时间内,没有线程。没有线程池线程,没有操作系统线程,什么都没有 - Stephen Cleary
谢谢Cleary先生。我相信我懂你的意思。我也认为我理解了你所描述的死锁问题。我想到的工作线程实际上与DownloadAsync方法无关,只是通过回调进入其中。当注册oncompletion回调并等待任务结果彼此等待时会发生死锁。 - Gerard
显示剩余3条评论

3

new Task会使用TaskScheduler.Current来执行整个方法,通常这会利用ThreadPool

通过使用async/await,该方法是同步输入的,只有在需要时才会使用异步继续。

我的意思可以用以下LINQPad程序来演示:

const int delay = 1;

public async Task DoSomethingAsync()
{
    Thread.CurrentThread.ManagedThreadId.Dump();
    await Task.Delay(delay).ConfigureAwait(false);
    Thread.CurrentThread.ManagedThreadId.Dump();
}

void Main()
{
    DoSomethingAsync().Wait();  
}

尝试将delay更改为0,您将看到继续在相同的线程上恢复,这是因为如果没有延迟,Task.Delay会立即返回,避免了在不需要时安排和执行continuation的开销。
通过使用new Task,您失去了此巧妙功能,并且始终使用ThreadPool线程,即使异步方法的实现者可能认为它不必要。

@Gerard Yeh,async/await有点棘手,特别是在凌晨1点15分解释,我可能会在早上尝试改进它。 - Lukazoid
没有异步的方法完全在工作线程中运行,有异步等待的方法使用调用线程直到等待点,然后在工作线程中运行,除非像您的delay(0)示例一样没有真正的任务,它会再次在调用线程中运行。 - Gerard
1
@Gerard,你理解得完全正确。想象一下,如果你的DownloadAsync开始缓存结果,第二次调用就可以立即返回。如果你使用了new Task,那么为了返回缓存值,你将使用线程池线程。使用async/await时,调用者不需要担心这些实现细节,只需说出当准备好结果时给我即可。 - Lukazoid
非常感谢您如此晚才回复,我现在明白了。 - Gerard
1
new Task() 不执行任何内容。而 Task.Start() 使用 TaskScheduler.Current 执行它。这通常与 TaskScheduler.Default 相同,但并非总是如此。 - svick
显示剩余4条评论

0

看看这个帖子,Stephen Cleary准确地解释了为什么和区别。

简单来说,它们非常相似。这是等待的新旧方式。我发现async / await更好看,因为在其他方法中需要额外的代码,并且不需要放置task.Start()。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接