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

Ark*_*kun 4 .net c# multithreading asynchronous async-await

简短的问题:

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

详细问题:

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

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

假设您有两行代码:

F1();
F2();
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

如果我们要运行F1F2在同时,我们可以这样写:

var task1 = Task.Run(F1);
F2();
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

你可能会说*Async方法使用一些特殊的魔法(比如处理外部信号),使它们比同步方法更有效.事情是,我不认为这是事实.

我们来看看Stream.ReadAsync例如.如果你看一下源代码,ReadAsync就浪费几百行铃声和口哨代码来创建一个只调用同步Read方法的任务.那为什么我们需要呢?为什么不直接使用Task.RunStream.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<int>();
            while (!testTask.IsCompleted) {
                var threadIds = Process.GetCurrentProcess().Threads.OfType<ProcessThread>().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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

此代码尝试运行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
Run Code Online (Sandbox Code Playgroud)

这意味着:

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

Ste*_*ary 16

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

这根本不起作用async.看我的async介绍.

你可能会说*Async方法使用一些特殊的魔法(比如处理外部信号),使它们比同步方法更有效.事情是,我不认为这是事实.

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

我们来看看Stream.ReadAsync.

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

对于一个更合适的例子,比较之间的差异WebClientHttpClient.

  • `Task.Run`和`Task.Factory.StartNew`都使用线程池,如果你使用它们来调用同步方法,它们将阻塞一个线程池线程.大多数`*Async`库方法最终调用`TaskFactory.FromAsync`或`TaskCompletionSource`,它们不会阻塞线程池线程. (4认同)

nos*_*tio 5

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

首先,我们扩展了以下标准限制ThreadPool:

ThreadPool.SetMaxThreads(MAX_REQS * 2, MAX_REQS * 2);
ThreadPool.SetMinThreads(MAX_REQS, MAX_REQS);
Run Code Online (Sandbox Code Playgroud)

注意我请求相同数量的workerThreadscompletionPortThreads.然后我们将MAX_REQS使用每个API对bing.com 执行= 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<string> 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<Task> 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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

结果:

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

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