为什么同步代码在异步等待的任务中比异步代码慢得多

FCi*_*Cin 6 c# wpf asynchronous async-await

我一直在玩无聊,同时从wiki中检索随机文章.首先我写了这段代码:

private async void Window_Loaded(object sender, RoutedEventArgs e)
{
    await DownloadAsync();
}

private async Task DownloadAsync()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        var tasks = new List<Task>();
        var result = new List<string>();

        for (int index = 0; index < 60; index++)
        {
            var task = Task.Run(async () => {
                var scheduledAt = DateTime.UtcNow.ToString("mm:ss.fff");
                using (var client = new HttpClient())
                using (var response = await client.GetAsync("https://en.wikipedia.org/wiki/Special:Random"))
                using (var content = response.Content)
                {
                    var page = await content.ReadAsStringAsync();
                    var receivedAt = DateTime.UtcNow.ToString("mm:ss.fff");
                    var data = $"Job done at thread: {Thread.CurrentThread.ManagedThreadId}, Scheduled at: {scheduledAt}, Recieved at: {receivedAt} {page}";
                    result.Add(data);
                }
            });

            tasks.Add(task);
        }

        await Task.WhenAll(tasks.ToArray());

        sw.Stop();
        Console.WriteLine($"Process took: {sw.Elapsed.Seconds} sec {sw.Elapsed.Milliseconds} ms");

        foreach (var item in result)
        {
            Debug.WriteLine(item);
        }
    }
Run Code Online (Sandbox Code Playgroud)

但我想摆脱这种异步匿名方法:Task.Run(async () => ...所以我将相关的代码部分替换为:

for (int index = 0; index < 60; index++)
{
    var task = Task.Run(() => {
        var scheduledAt = DateTime.UtcNow.ToString("mm:ss.fff");
        using (var client = new HttpClient())
        // Get this synchronously.
        using (var response = client.GetAsync("https://en.wikipedia.org/wiki/Special:Random").Result)
        using (var content = response.Content)
        {
            // Get this synchronously.
            var page = content.ReadAsStringAsync().Result;
            var receivedAt = DateTime.UtcNow.ToString("mm:ss.fff");
            var data = $"Job done at thread: {Thread.CurrentThread.ManagedThreadId}, Scheduled at: {scheduledAt}, Recieved at: {receivedAt} {page}";
            result.Add(data);
        }
    });

    tasks.Add(task);
}
Run Code Online (Sandbox Code Playgroud)

我期望它执行完全相同,因为我用同步替换的异步代码被包装在一个任务中,所以我保证任务调度程序(WPF任务调度程序)将它从ThreadPool的一些空闲线程上排队.这正是我看到返回的结果时发生的事情,我得到的值如下:

Job done at thread: 6, Scheduled at: 53:57.534, Recieved at: 54:54.545 ...
Job done at thread: 21, Scheduled at: 54:06.742, Recieved at: 54:54.574 ...
Job done at thread: 41, Scheduled at: 54:26.742, Recieved at: 54:54.576 ...
Job done at thread: 10, Scheduled at: 53:59.018, Recieved at: 54:54.614 ...
Run Code Online (Sandbox Code Playgroud)

问题是第一个代码在~6秒内执行,第二个代码(同步.Result)需要约50秒.随着减少任务数量,差异变小.任何人都可以解释为什么他们花了这么长时间,即使他们在不同的线程上执行并执行完全相同的单一操作?

Evk*_*Evk 6

因为线程池可能在您请求新线程时引入延迟,所以如果池中的线程总数大于可配置的最小值.最低限度是number of cores默认值.在示例中.Result,您对60个任务进行排队,这些任务在执行的整个持续时间内都包含线程池线程.这意味着只有number of cores任务才会立即启动,然后休息将以延迟开始(如果已经忙碌的线程可用,则线程池将等待一定时间,如果没有,则将添加新线程).

更糟糕的是 - client.GetAsync(GetAsync从服务器收到回复后在函数内部执行的代码)的延续也被安排到线程池线程.这包含了所有60个任务,因为它们在接收结果之前无法完成GetAsync,并且GetAsync需要免费的线程池线程来运行其继续.结果,还有一个额外的争用:你创建了60个任务,并且还有60个延续GetAsync,也希望线程池线程能够运行(而你的60个任务被阻塞,等待那些延续的结果).

在示例中await- 在异步http调用的持续时间内释放线程池线程.因此,当您调用await GetAsync()GetAsync达到异步IO点(实际上发出http请求)时 - 您的线程将被释放回池中.现在它可以自由处理其他请求.这意味着await示例保持线程池线程的时间要少得多,并且在等待线程池线程变得可用时几乎没有延迟.

您可以通过执行操作轻松确认(请勿使用真实代码,仅用于测试)

ThreadPool.SetMinThreads(100, 100);
Run Code Online (Sandbox Code Playgroud)

增加上面提到的池中可配置的最小线程数.当您将其增加到较大值时 - 示例中的所有60个任务.Result将在60个线程池线程上同时启动,没有延迟,因此您的示例将在大致相同的时间内完成.

以下是示例应用程序,以观察它的工作原理:

public class Program {
    public static void Main(string[] args) {
        DownloadAsync().Wait();
        Console.ReadKey();
    }

    private static async Task DownloadAsync() {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        var tasks = new List<Task>();
        for (int index = 0; index < 60; index++) {
            var tmp = index;
            var task = Task.Run(() => {
                ThreadPool.GetAvailableThreads(out int wt, out _);
                ThreadPool.GetMaxThreads(out int mt, out _);
                Console.WriteLine($"Started: {tmp} on thread {Thread.CurrentThread.ManagedThreadId}. Threads in pool: {mt - wt}");
                var res = DoStuff(tmp).Result;
                Console.WriteLine($"Done {res} on thread {Thread.CurrentThread.ManagedThreadId}");
            });

            tasks.Add(task);
        }

        await Task.WhenAll(tasks.ToArray());

        sw.Stop();
        Console.WriteLine($"Process took: {sw.Elapsed.Seconds} sec {sw.Elapsed.Milliseconds} ms");
    }

    public static async Task<string> DoStuff(int i) {
        await Task.Delay(1000); // web request
        Console.WriteLine($"continuation of {i} on thread {Thread.CurrentThread.ManagedThreadId}"); // continuation
        return i.ToString();
    }
}
Run Code Online (Sandbox Code Playgroud)