好的,所以我已经读了很多书,并研究了使用async methods
和task
等的最佳方法。我相信我(大部分)理解它,但我想检查一下以确保它。
我确实开始使用Task.Run()
为同步任务制作async wrappers
,但最近读到,最好只有一个sync method
,并让应用程序通过使用Task.Run()
本身来确定是否需要将其推送到另一个线程,这是有道理的。然而,我也读到,这方面的例外情况是自然async functions
。我不确定我是否完全理解"自然异步",但作为基本理解,提供async methods
的.NET框架方法似乎是naturally async
,WebClient.DownloadFile/DownlodFileAsync
就是其中之一。
所以我有两种方法,如果有人愿意提供反馈,我想测试我的理解,并确保这是正确的。
方法一是在本地操作系统上移动一些文件,看起来像这样(伪代码):
Public static void MoveStagingToCache()
{
...do some file move, copy, delete operations...
}
第二个看起来像这样(伪代码):
Public static void DownloadToCache()
{
...do some analysis to get download locations...
using (var wc = new WebClient())
{
wc.DownloadFile(new Uri(content.Url), targetPath);
}
...do other stuff...
}
因此,我的理解如下。第一种方法应该保持原样,因为没有一种文件操作方法具有async versions
,因此不太可能是自然的async
。这将由调用者决定是仅调用MoveStagingToCache()
来运行同步,还是调用Task.Run(()=>MoveStagingToCache())
来将其推送到后台线程。
然而,在第二种方法中,下载自然是async
(除非我误解了),所以它可以创建同步和异步版本。为了做到这一点,我不应该只是像这样包装同步方法:
Public static Task DownloadToCacheAsync()
{
return Task.Run(()=>DownloadToCache());
}
相反,我应该使核心方法异步如下:
Public static async Task DownloadToCache()
{
...do some analysis to get download locations...
using (var wc = new WebClient())
{
await wc.DownloadFileTaskAsync(new Uri(content.Url), targetPath);
}
...do other stuff...
}
然后我可以创建这样的同步版本:
Public static void DownloadToCache()
{
DownloadToCacheAsync().Wait();
}
这允许使用自然异步方法,也为那些需要它的人提供了同步过载
这是对系统的一个很好的理解,还是我搞砸了什么?
顺便说一句,WebClient.DownloadFileAsync
和WebClient.DownloadFileTaskAsync
的区别是否只是任务返回了一个用于错误捕获的任务?
编辑
好的,所以在评论和答案中进行了一些讨论后,我意识到我应该添加更多关于我的系统和预期用途的细节。这是在一个旨在在桌面上运行的库中,而不是ASP。因此,我不关心保存用于请求处理的线程,主要关心的是保持UI线程打开并对用户做出响应,并在用户继续做他们需要做的事情时将"后台"任务推送到系统可以处理的另一个线程
对于MoveStagingToCache
,这将在程序启动时调用,但我不需要等待它完成才能继续加载或使用程序。在大多数情况下,它会在程序的其余部分启动和运行之前完成,并让用户做任何最有可能的事情(可能最多5-10秒的运行时间),但即使在用户交互开始之前没有完成,程序也会正常工作。正因为如此,我在这里的主要愿望是将此操作从UI线程中移除,开始工作,然后继续执行程序。
因此,对于这个方法,我目前的理解是,这个方法应该在库中同步,但当我从主(UI)线程调用它时,我只使用
Task.Run(()=>MoveStagingToCache());
既然我真的不需要在完成后做任何事情,我真的就不需要等待了,对吧?如果我只做上面的操作,它会在后台线程上启动操作吗?
对于DownloadToCache
,相似但略有不同。我希望用户能够启动下载操作,然后继续在UI中工作,直到下载完成。完成后,我需要做一些简单的操作来通知用户它已经准备好了,并启用"使用它"按钮等。在这种情况下,我的理解是,我会将其创建为等待WebClient
下载调用的异步方法。这将把它从UI线程中推出来进行实际下载,但一旦下载完成,它就会返回,允许我在等待调用后进行任何需要的UI更新。
正确吗?
您不应该为同步方法编写异步包装器,但也不应该为异步方法编写同步包装器-这两个都是反模式。
提示:"自然异步">多数表示基于I/O,但也有少数例外。不幸的是,其中一个例外是一些文件系统操作,包括移动文件,应该是异步的,但API不支持异步,所以我们不得不假装它是同步的。
在您的情况下,DownloadToCache
肯定是自然异步的。在这些情况下,我更喜欢只公开异步API。
如果您必须(或者真的想要)也支持同步API,我推荐布尔参数破解。语义是,如果传入sync:true
,则返回的任务已经完成。这允许您将逻辑保持在一个方法中,并编写非常小的包装器,而不会出现通常与这些包装器相关的陷阱:
private static async Task DownloadToCacheAsync(bool sync)
{
...do some analysis to get download locations...
using (var wc = new WebClient())
{
if (sync)
wc.DownloadFile(new Uri(content.Url), targetPath);
else
await wc.DownloadFileTaskAsync(new Uri(content.Url), targetPath);
}
...do other stuff...
}
public static Task DownloadToCacheAsync() => DownloadToCacheAsync(sync: false);
public static void DownloadToCache() => DownloadToCacheAsync(sync: true).GetAwaiter().GetResult();
您提出了多个相互交织的问题,让我尝试回答其中的大多数问题,首先阅读Stephen Cleary的文章,以更清楚地了解异步的工作,特别是关于自然异步的工作-没有线程,这将有助于理解自然异步方法的含义。
异步操作
Async operations IO bound and CPU bound
有两种
绑定到IO的异步如何工作
IO绑定是离开进程边界对外部服务(数据库、Web服务、Rest API)进行调用的操作,对于这些操作,同步调用会导致阻塞线程,等待进程外调用返回,但在异步操作的情况下,可以释放调用线程来处理其他调用。
IO异步调用返回时发生了什么
当调用返回时,任何工作线程都可以被分配来完成调用,这可以在Task object
上使用ConfigureAwait(false)
来指示。通过IO绑定异步设计,系统可以实现非常高的可扩展性,因为线程是每个进程宝贵/有限的资源,不会浪费在空闲等待中。
真异步/自然异步/IO异步
一个True Async请求可以服务数百万个请求,而同步系统的请求只有100个,底层技术在windows中被称为IO completion ports
。它不需要软件线程来处理请求,它使用基于硬件的并发
CPU绑定异步
另一方面,对于CPU绑定的异步用例,是在WPF或类似的厚客户端的情况下,它们可以使用Task.Run
在单独的线程上进行后台处理,从而释放Ui线程,使系统的响应能力更强,这里不保存线程,因为工作线程是使用Task.Run
调用的,它都在同一进程中,但系统的响应性更强,这是通过异步处理的一个重要成就。
我确实开始使用Task.Run()为同步任务制作异步包装器,但最近读到,最好只有一个同步方法,让应用程序确定是否需要使用Task.Run()将其推送到另一个线程
应用程序没有自动的方法来决定,是您的代码还是您正在调用的代码来创建工作线程。对于绑定到IO的异步,您需要调用正确的API,该API将在内部利用Windows(IO完成端口)公开的异步管道,使其成为纯异步
我不确定我是否完全理解"自然异步",但作为基本理解,提供异步方法的.NET框架方法似乎是自然异步的,WebClient.DownloadFile/DownlodFileAsync就是其中之一
大多数提供Async版本的.Net方法都是IO绑定调用,CPU绑定需要使用Task.Run
显式创建,然后等待。对于基于事件的异步实现,TaskCompletionSource
是一个很好的选项,它返回一个正在进行的可退出的Task
,可以是SetResult
、SetException
,用于设置事件中的结果或错误,从而异步完成Task
。TaskCompletionSource
也是一个无线程实现
关于您发布的不同方法
您对Method1
的理解是正确的,用户可以决定是否使用后台线程来调用它,因为它释放了可以是Ui线程的主调用线程。关于Method2
,正如评论中所提到的,确保它一直是Async到顶部(入口点),因为这是释放调用线程以用于其他操作的唯一方法
最后
这个代码是个非常糟糕的主意,@LexLi 在评论中也提到了这一点
Public static Task DownloadToCacheAsync()
{
return Task.Run(()=>DownloadToCache());
}
Public static void DownloadToCache()
{
DownloadToCacheAsync().Wait();
}
几点:
- 在绑定IO的Synchronous方法上运行Task.不会有任何优势,因为工作线程无论如何都会被阻塞,因此线程处于空闲等待状态。与Async方法不同,由于线程被阻塞,系统可扩展性没有任何增益
- 如果一个方法有
Async
版本,那么它肯定会有Sync
版本,需要简单地使用DownloadToCache()
。同步先于异步,我不知道只有异步调用而没有同步的情况 - 在调用线程中执行显式
Task.Wait()
,这使得系统没有响应,甚至在少数情况下会导致死锁,因为主线程被阻塞,而其他操作需要在主/调用线程上进行 - 使用
Task.Wait()
的唯一原因是在后台/工作线程/线程池线程上处理一些逻辑,这些逻辑需要等待,并且可能在进一步的逻辑处理之前使用结果,主要用于CPU绑定操作。这不适用于IO绑定操作
我希望这有助于澄清异步Await api 的使用