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秒.随着减少任务数量,差异变小.任何人都可以解释为什么他们花了这么长时间,即使他们在不同的线程上执行并执行完全相同的单一操作?
因为线程池可能在您请求新线程时引入延迟,所以如果池中的线程总数大于可配置的最小值.最低限度是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)