方法的异步和同步版本

Async and sync versions of method

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

我确实开始使用 Task.Run() 为同步任务制作 async wrappers,但最近读到只有 sync method 并让应用程序确定是否需要推送会更好通过使用 Task.Run() 本身到另一个线程,这是有道理的。但是,我还读到,这个例外自然是 async functions。我不确定我是否完全理解 'naturally async' 但作为基本理解似乎提供 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...
}

所以我的理解如下。第一种方法应该保留原样,因为 none 的文件操作方法有 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 的区别只是任务 return 是错误捕获任务吗?

编辑

好的,在评论和回答中进行了一些讨论后,我意识到我应该在我的系统和预期用途上添加更多细节。这是在一个库中,旨在 运行 在桌面上,而不是 ASP。因此,我不关心为请求处理保存线程,主要关心的是保持 UI 线程打开并响应用户,并将 'background' 任务推送到另一个可以处理的线程在用户做他们需要做的事情的同时由系统执行。

对于MoveStagingToCache,这将在程序启动时调用,但我不需要等待它完成继续加载或使用程序。在大多数情况下,它会在程序的其余部分启动和 运行ning 之前完成,并让用户做任何最有可能的事情(可能 5-10 秒 运行 时间最长),但即使它没有完成在用户交互开始之前,该程序将正常运行。因此,我在这里的主要愿望是将此操作从 UI 线程中移出,使其开始工作,然后继续执行该程序。

所以对于这个,我目前的理解是这个方法应该在库中同步,但是当我从主 (UI) 线程调用它时,我只使用

Task.Run(()=>MoveStagingToCache());

因为完成后我真的不需要做任何事情,所以我真的不需要等待这个对吧?如果我只执行上述操作,它会在后台线程上启动操作吗?

对于DownloadToCache,相似但有一点不同。我希望用户能够启动下载操作,然后在 UI 中继续工作,直到下载完成。完成后,我需要做一些简单的操作来通知用户它已准备就绪并启用 'use it' 按钮等。在这种情况下,我的理解是我会将其创建为等待的异步方法WebClient 下载调用。这会将它推离 UI 线程以进行实际下载,但是一旦下载完成后 return 将允许我在 await 调用后执行任何需要的 UI 更新。

正确吗?

您提出了多个相互交织的问题,让我尝试回答其中的大部分问题,首先阅读 Stephen Cleary for much more clarity on the working of Async, especially regarding Naturally Async - There's no thread 的文章,这将有助于理解自然异步方法的含义。

Async Operations

有两种Async operations IO bound and CPU bound

How does IO bound Async works

IO bound 是离开进程边界调用外部服务(数据库、Web 服务、Rest API)的操作,同步调用会导致线程阻塞,空闲等待对 return 的进程外调用,但在异步操作的情况下,可以释放调用线程以处理其他调用。

What happens on IO Async call return

当调用returns时,可以分配任何工作线程来完成调用,这可以用ConfigureAwait(false)表示上一个Task object。凭借 IO 绑定异步设计,系统可以实现非常高的可扩展性,因为线程是每个进程的宝贵/有限资源,不会浪费在空闲等待中。

True Async / Natural Async / IO Async

一个真正的异步请求可以服务数百万个请求,相比之下同步系统只有几百个,底层技术在windows中被称为IO completion ports。它不需要软件线程来处理请求,它使用基于硬件的并发

CPU Bound Async

另一方面,CPU 绑定异步用例是在 WPF 或类似胖客户端的情况下,它可以使用 Task.Run 在单独的线程上进行后台处理,从而释放Ui 线程,保持系统响应更快,这里你不保存线程,因为工作线程是使用 Task.Run 调用的,它都在同一个进程中,但系统响应更快,这是通过异步处理取得的重要成就。

I did start making async wrappers for sync tasks using Task.Run() but but recently read that it's much better to just have a sync method and let the application determine if it needs to be pushed to another thread by using Task.Run()

应用程序无法自动决定是您的代码还是您正在调用的代码创建工作线程。对于 IO 绑定 Async,您需要调用正确的 API,这将在内部利用 Windows(IO 完成端口)公开的 Async 管道使其成为纯 Async

I'm not sure I fully understand 'naturally async' but as a basic understanding it seems that .NET framework methods that offer async methods are naturally async and WebClient.DownloadFile/DownlodFileAsync is one of those.

大多数提供异步版本的 .Net 方法都是 IO 绑定调用,CPU 绑定需要使用 Task.Run 显式创建,然后等待。对于基于事件的异步实现,TaskCompletionSource 是一个不错的选择,return 是一个正在进行的等待 Task,可以是 SetResultSetException 用于设置结果或事件中的错误从而异步完成 TaskTaskCompletionSource也是无线程实现

Regarding different methods that you have posted

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

Finally

这段代码非常糟糕,@LexLi 在评论中也提到了

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

Public static void DownloadToCache()
{
    DownloadToCacheAsync().Wait();
}

两点:

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

我希望这有助于提供有关 Async-Await api 用法的一些清晰信息

shouldn't write asynchronous wrappers for synchronous methods, but you also shouldn't write synchronous wrappers for asynchronous methods - 这些都是 反模式。

提示:"naturally asynchronous" 大多数情况下 表示 I/O-based,但有少数例外。其中一个例外是一些文件系统操作,不幸的是,包括移动文件,应该 是异步的,但是 APIs 不支持异步,所以我们不得不假装它是同步。

在你的情况下,DownloadToCache 绝对是自然异步的。在那些情况下,我更喜欢公开异步 API only.

如果您必须(或者真的想要)也支持同步API,我推荐boolean argument hack。语义是如果你传入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();