Short question:
Why did .Net Framework add a lot of *Async versions of method instead of developers just using Task.Run
to run synchron
Let's do the realistic test: naturally asynchronous WebRequest.GetResponseAsync vs unnaturally synchronous WebRequest.GetResponse.
First, we extend the standard limits of the ThreadPool
:
ThreadPool.SetMaxThreads(MAX_REQS * 2, MAX_REQS * 2);
ThreadPool.SetMinThreads(MAX_REQS, MAX_REQS);
Note I request the same number of workerThreads
and completionPortThreads
. Then we'll perform MAX_REQS
= 200 parallel requests to bing.com, using each API.
The code (a standalone console app):
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();
}
}
}
Output:
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
The result:
Both tests takes about 50 seconds to complete, but GetStringAsync
peaks at 83 threads, while GetStringSync
does at 213. The higher the MAX_REQS
figure goes, the more threads are wasted by the blocking WebRequest.GetResponse
API.
@Ark-kun, I hope you see the point now.
Marking the methods async means that we can use the await keyword to basically schedule the heavy processing on another thread and free the UI thread until the processing is finished.
That's not at all how async
works. See my async intro.
You may say that the *Async methods use some special magic (like handling external signals) that make them more efficient than their synchronous counterparts. The thing is that I don't see this beeing the case.
In pure asynchronous code, there is no thread (as I explain on my blog). In fact, at the device driver level, all (non-trivial) I/O is asynchronous. It is the synchronous APIs (at the OS level) that are an abstraction layer over the natural, asynchronous APIs.
Let's look at the Stream.ReadAsync for example.
Stream
is an unusual case. As a base class, it has to prevent breaking changes as much as possible. So, when they added the virtual ReadAsync
method, they had to add a default implementation. This implementation has to use a non-ideal implementation (Task.Run
), which is unfortunate. In an ideal world, ReadAsync
would be (or call) an abstract asynchronous implementation, but that would break every existing implementation of Stream
.
For a more proper example, compare the difference between WebClient
and HttpClient
.