考虑到使用Task.Run异步运行任何方法的能力,.Net Framework中的* Async方法的目的是什么?

简短的问题:

为什么.Net Framework添加了很多* Async版本的方法而不是开发人员只是使用Task.Run来异步运行同步方法?

详细问题:

  • 我理解异步性的概念。
  • 我知道Tasks
  • 我知道async / await关键字。
  • 我知道.Net Framework中的* Async方法有什么用。

我不明白的是库中* Async方法的目的。

假设您有两行代码:

 F1(); F2(); 

关于数据/控制流程,只有两种情况:

  • F1完成后需要执行F2
  • F2不需要等待F1完成。

我没有看到任何其他情况。 我没有看到任何一般需要知道执行某些function的具体线程(除了UI)。 线程中代码的基本执行模式是同步的。 并行性需要多个线程。 异步性基于并行性和代码重新排序。 但基地仍然是同步的。

F1的工作量很小时,差异无关紧要。 但是当A花费大量时间完成时,我们可能需要查看情况,如果F2不需要等待F1完成,我们可以与F2并行运行F1

很久以前我们使用线程/线程池来做到这一点。 现在我们有Tasks

如果我们想并行运行F1F2 ,我们可以写:

 var task1 = Task.Run(F1); F2(); 

任务很酷,我们可以在最终需要完成任务的地方使用await

到目前为止,我认为没有必要制作F1Async()方法。

现在,我们来看一些特殊情况。 我看到的唯一真正特殊情况是UI。 UI线程是特殊的并且停止它会使UI冻结很糟糕。 正如我所看到的,Microsoft建议我们将UI事件处理程序标记为async 。 将方法标记为async意味着我们可以使用await关键字基本上在另一个线程上调度繁重的处理并释放UI线程,直到处理完成。

我不能再得到的是为什么我们需要任何* Async方法来等待它们。 我们总是可以写await Task.Run(F1); 。 为什么我们需要F1Async

你可能会说* Async方法使用一些特殊的魔法(比如处理外部信号),使它们比同步方法更有效。 问题是我没有看到这样的情况。

我们来看看Stream.ReadAsync 。 如果你看一下源代码, ReadAsync浪费数百行铃声和口哨代码来创建一个只调用同步Read方法的任务。 那为什么我们需要呢? 为什么不直接使用Task.Run使用Stream.Read

这就是为什么我不了解通过创建同步方法的普通*异步副本来膨胀库的原因。 MS甚至可以添加语法糖,这样我们就可以编写await async Stream.Read而不是await Stream.ReadAsyncTask.Run(Stream.Read)

现在您可能会问“为什么不将* Async方法作为唯一方法并删除同步方法?”。 正如我之前所说,基本代码执行模式是同步的。 异步运行同步方法很容易,但不是另一种方式。

那么,.Net Framework中* Async方法的目的是什么,因为能够使用Task.Run异步运行任何方法?

PS如果非冻结UI非常重要,为什么不默认运行处理程序异步并防止任何冻结的可能性?

“无线程”论点:

回答这个问题的人似乎暗示* Async方法的优点是它们是高效的,因为它们不会创建新的线程。 问题是我没有看到这种行为。 并行异步任务的行为与我想的一样 – 为每个并行任务创建(或从线程池中获取)线程(并非所有任务都是并行执行)。

这是我的测试代码:

 using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace ConsoleApplication32167 { class Program { static async Task TestAsync() { var httpClient = new HttpClient() { Timeout = TimeSpan.FromMinutes(20) }; var tasks = Enumerable.Range(1, 100).Select((i) => httpClient.GetStringAsync("http://localhost/SlowWebsite/")); Console.WriteLine("Threads before completion: " + Process.GetCurrentProcess().Threads.Count); await Task.WhenAll(tasks); Console.WriteLine("Threads after completion: " + Process.GetCurrentProcess().Threads.Count); } static void Main(string[] args) { Console.WriteLine("Threads at start: " + Process.GetCurrentProcess().Threads.Count); var timer = new Stopwatch(); timer.Start(); var testTask = TestAsync(); var distinctThreadIds = new HashSet(); while (!testTask.IsCompleted) { var threadIds = Process.GetCurrentProcess().Threads.OfType().Select(thread => thread.Id).ToList(); distinctThreadIds.UnionWith(threadIds); Console.WriteLine("Current thread count: {0}; Cumulative thread count: {1}.", threadIds.Count, distinctThreadIds.Count); Thread.Sleep(250); } testTask.Wait(); Console.WriteLine(timer.Elapsed); Console.ReadLine(); } } } 

此代码尝试运行100个HttpClient.GetStringAsync任务,向网站发出请求,需要1分钟才能响应。 同时,它计算活动线程的数量和进程创建的不同累积数量。 正如我所预测的,这个程序会创建许多新线程。 输出如下:

 Current thread count: 4; Cumulative thread count: 4. .... Current thread count: 25; Cumulative thread count: 25. .... Current thread count: 7; Cumulative thread count: 63. Current thread count: 9; Cumulative thread count: 65. 00:10:01.9981006 

这意味着:

  • 在异步任务执行过程中创建了61个新线程。
  • 新活动线程的峰值数为21。
  • 执行需要多10倍的时间(10分钟而不是1分钟)。 这是由本地IIS限制引起的。

将方法标记为异步意味着我们可以使用await关键字基本上在另一个线程上调度繁重的处理并释放UI线程,直到处理完成。

这根本不是async工作方式。 看我的async介绍 。

你可能会说* Async方法使用一些特殊的魔法(比如处理外部信号),使它们比同步方法更有效。 问题是我没有看到这样的情况。

在纯异步代码中, 没有线程 (正如我在我的博客上解释的那样)。 实际上,在设备驱动程序级别, 所有 (非平凡的)I / O都是异步的。 同步API(在OS级别)是自然异步API的抽象层。

我们来看看Stream.ReadAsync。

Stream是一个不寻常的案例。 作为基类,它必须尽可能地防止更改。 因此,当他们添加虚拟ReadAsync方法时,他们必须添加默认实现。 此实现必须使用非理想的实现( Task.Run ),这是不幸的。 在理想的世界中, ReadAsync将(或调用)一个抽象的异步实现,但这会破坏Stream每个现有实现。

有关更恰当的示例,请比较WebClientHttpClient之间的差异。

让我们做真实的测试:自然异步WebRequest.GetResponseAsync与非自然同步的WebRequest.GetResponse

首先,我们扩展ThreadPool的标准限制:

 ThreadPool.SetMaxThreads(MAX_REQS * 2, MAX_REQS * 2); ThreadPool.SetMinThreads(MAX_REQS, MAX_REQS); 

注意我请求相同数量的workerThreadscompletionPortThreads 。 然后我们将使用每个API对bing.com执行MAX_REQS = 200个并行请求。

代码 (一个独立的控制台应用程序):

 using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using System.Linq; using System.Collections.Generic; using System.Net; namespace Console_21690385 { class Program { const int MAX_REQS = 200; // implement GetStringAsync static async Task GetStringAsync(string url) { using (var response = await WebRequest.Create(url).GetResponseAsync()) using (var stream = response.GetResponseStream()) using (var reader = new System.IO.StreamReader(stream)) { return await reader.ReadToEndAsync(); } } // test using GetStringAsync static async Task TestWithGetStringAsync() { var tasks = Enumerable.Range(1, MAX_REQS).Select((i) => GetStringAsync("http://www.bing.com/search?q=item1=" + i)); Console.WriteLine("Threads before completion: " + Process.GetCurrentProcess().Threads.Count); await Task.WhenAll(tasks); Console.WriteLine("Threads after completion: " + Process.GetCurrentProcess().Threads.Count); } // implement GetStringSync static string GetStringSync(string url) { using (var response = WebRequest.Create(url).GetResponse()) using (var stream = response.GetResponseStream()) using (var reader = new System.IO.StreamReader(stream)) { return reader.ReadToEnd(); } } // test using GetStringSync static async Task TestWithGetStringSync() { var tasks = Enumerable.Range(1, MAX_REQS).Select((i) => Task.Factory.StartNew( () => GetStringSync("http://www.bing.com/search?q=item1=" + i), CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default)); Console.WriteLine("Threads before completion: " + Process.GetCurrentProcess().Threads.Count); await Task.WhenAll(tasks); Console.WriteLine("Threads after completion: " + Process.GetCurrentProcess().Threads.Count); } // run either of the tests static void RunTest(Func runTest) { Console.WriteLine("Threads at start: " + Process.GetCurrentProcess().Threads.Count); var stopWatch = new Stopwatch(); stopWatch.Start(); var testTask = runTest(); while (!testTask.IsCompleted) { Console.WriteLine("Currently threads: " + Process.GetCurrentProcess().Threads.Count); Thread.Sleep(1000); } Console.WriteLine("Threads at end: " + Process.GetCurrentProcess().Threads.Count + ", time: " + stopWatch.Elapsed); testTask.Wait(); } static void Main(string[] args) { ThreadPool.SetMaxThreads(MAX_REQS * 2, MAX_REQS * 2); ThreadPool.SetMinThreads(MAX_REQS, MAX_REQS); Console.WriteLine("Testing using GetStringAsync"); RunTest(TestWithGetStringAsync); Console.ReadLine(); Console.WriteLine("Testing using GetStringSync"); RunTest(TestWithGetStringSync); Console.ReadLine(); } } } 

输出:

 Testing using GetStringAsync Threads at start: 3 Threads before completion: 3 Currently threads: 25 Currently threads: 84 Currently threads: 83 Currently threads: 83 Currently threads: 83 Currently threads: 83 Currently threads: 83 Currently threads: 84 Currently threads: 83 Currently threads: 83 Currently threads: 84 Currently threads: 84 Currently threads: 84 Currently threads: 83 Currently threads: 83 Currently threads: 84 Currently threads: 83 Currently threads: 82 Currently threads: 82 Currently threads: 82 Currently threads: 83 Currently threads: 25 Currently threads: 25 Currently threads: 26 Currently threads: 25 Currently threads: 25 Currently threads: 25 Currently threads: 23 Currently threads: 23 Currently threads: 24 Currently threads: 20 Currently threads: 20 Currently threads: 19 Currently threads: 19 Currently threads: 19 Currently threads: 19 Currently threads: 18 Currently threads: 19 Currently threads: 19 Currently threads: 19 Currently threads: 18 Currently threads: 18 Currently threads: 18 Currently threads: 19 Currently threads: 19 Currently threads: 18 Currently threads: 19 Currently threads: 19 Currently threads: 18 Currently threads: 18 Currently threads: 17 Threads after completion: 17 Threads at end: 17, time: 00:00:51.2605879 Testing using GetStringSync Threads at start: 15 Threads before completion: 15 Currently threads: 55 Currently threads: 213 Currently threads: 213 Currently threads: 213 Currently threads: 213 Currently threads: 213 Currently threads: 213 Currently threads: 213 Currently threads: 213 Currently threads: 212 Currently threads: 210 Currently threads: 210 Currently threads: 210 Currently threads: 210 Currently threads: 210 Currently threads: 210 Currently threads: 210 Currently threads: 210 Currently threads: 210 Currently threads: 210 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 209 Currently threads: 205 Currently threads: 201 Currently threads: 196 Currently threads: 190 Currently threads: 186 Currently threads: 182 Threads after completion: 178 Threads at end: 173, time: 00:00:47.2603652 

结果:

两个测试都需要大约50秒才能完成,但GetStringAsync在83个线程处达到峰值,而GetStringSync在213处执行MAX_REQS数字越高,阻塞的WebRequest.GetResponse API浪费的线程就越多。

@Ark-kun,我希望你现在明白这一点。