方法的异步和同步版本



好的,所以我已经读了很多书,并研究了使用async methodstask等的最佳方法。我相信我(大部分)理解它,但我想检查一下以确保它。

我确实开始使用Task.Run()为同步任务制作async wrappers,但最近读到,最好只有一个sync method,并让应用程序通过使用Task.Run()本身来确定是否需要将其推送到另一个线程,这是有道理的。然而,我也读到,这方面的例外情况是自然async functions。我不确定我是否完全理解"自然异步",但作为基本理解,提供async methods的.NET框架方法似乎是naturally asyncWebClient.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.DownloadFileAsyncWebClient.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,可以是SetResultSetException,用于设置事件中的结果或错误,从而异步完成TaskTaskCompletionSource也是一个无线程实现

关于您发布的不同方法

您对Method1的理解是正确的,用户可以决定是否使用后台线程来调用它,因为它释放了可以是Ui线程的主调用线程。关于Method2,正如评论中所提到的,确保它一直是Async到顶部(入口点),因为这是释放调用线程以用于其他操作的唯一方法

最后

这个代码是个非常糟糕的主意,@LexLi 在评论中也提到了这一点

Public static Task DownloadToCacheAsync()
{
return Task.Run(()=>DownloadToCache());
}
Public static void DownloadToCache()
{
DownloadToCacheAsync().Wait();
}

几点:

  1. 在绑定IO的Synchronous方法上运行Task.不会有任何优势,因为工作线程无论如何都会被阻塞,因此线程处于空闲等待状态。与Async方法不同,由于线程被阻塞,系统可扩展性没有任何增益
  2. 如果一个方法有Async版本,那么它肯定会有Sync版本,需要简单地使用DownloadToCache()。同步先于异步,我不知道只有异步调用而没有同步的情况
  3. 在调用线程中执行显式Task.Wait(),这使得系统没有响应,甚至在少数情况下会导致死锁,因为主线程被阻塞,而其他操作需要在主/调用线程上进行
  4. 使用Task.Wait()的唯一原因是在后台/工作线程/线程池线程上处理一些逻辑,这些逻辑需要等待,并且可能在进一步的逻辑处理之前使用结果,主要用于CPU绑定操作。这不适用于IO绑定操作

我希望这有助于澄清异步Await api 的使用

相关内容

  • 没有找到相关文章

最新更新