ThreadPool
actually maintains two sub-pools, one for worker threads, and another for IOCP threads. When the result of GetAsync
is available, a random IOCP thread (I/O Completion Port) gets allocated from the pool to handle the completion of the async HTTP request. That's where the code after await
gets executed. You have control over the size of each sub-pool with ThreadPool.SetMinThreads/SetMaxThreads
, more about this below.
As is, your non-async code can hardly be compared to the async version. For a more fair comparison, you should stick to WebRequest
for both cases, e.g.:
Non-Async:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace NoAsync
{
internal class Program
{
private const int totalCalls = 100;
private static void Main(string[] args)
{
int maxWorkers, maxIOCPs;
ThreadPool.GetMaxThreads(out maxWorkers, out maxIOCPs);
int minWorkers, minIOCPs;
ThreadPool.GetMinThreads(out minWorkers, out minIOCPs);
Console.WriteLine(new { maxWorkers, maxIOCPs, minWorkers, minIOCPs });
ThreadPool.SetMinThreads(100, 100);
var tasks = new List<Task>();
for (int i = 1; i <= totalCalls; i++)
tasks.Add(Task.Run(() => GoogleSearch(i)));
Task.WaitAll(tasks.ToArray());
}
private static void GoogleSearch(object searchTerm)
{
string url = @"https://www.google.com/search?q=" + searchTerm;
Console.WriteLine("Total number of threads in use={0}", Process.GetCurrentProcess().Threads.Count);
WebRequest wr = WebRequest.Create(url);
var httpWebResponse = (HttpWebResponse)wr.GetResponse();
var reader = new StreamReader(httpWebResponse.GetResponseStream());
string responseFromServer = reader.ReadToEnd();
//Console.WriteLine(responseFromServer); // Display the content.
reader.Close();
httpWebResponse.Close();
Console.WriteLine("Total number of threads in use={0}", Process.GetCurrentProcess().Threads.Count);
}
}
}
Async:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace Async
{
internal class Program
{
private const int totalCalls = 100;
private static void Main(string[] args)
{
int maxWorkers, maxIOCPs;
ThreadPool.GetMaxThreads(out maxWorkers, out maxIOCPs);
int minWorkers, minIOCPs;
ThreadPool.GetMinThreads(out minWorkers, out minIOCPs);
Console.WriteLine(new { maxWorkers, maxIOCPs, minWorkers, minIOCPs });
ThreadPool.SetMinThreads(100, 100);
var tasks = new List<Task>();
for (int i = 1; i <= totalCalls; i++)
tasks.Add(GoogleSearch(i));
Task.WaitAll(tasks.ToArray());
}
private static async Task GoogleSearch(object searchTerm)
{
string url = @"https://www.google.com/search?q=" + searchTerm;
Console.WriteLine("Total number of threads in use={0}", Process.GetCurrentProcess().Threads.Count);
WebRequest wr = WebRequest.Create(url);
var httpWebResponse = (HttpWebResponse) await wr.GetResponseAsync();
var reader = new StreamReader(httpWebResponse.GetResponseStream());
string responseFromServer = await reader.ReadToEndAsync();
//Console.WriteLine(responseFromServer); // Display the content.
reader.Close();
httpWebResponse.Close();
Console.WriteLine("Total number of threads in use={0}", Process.GetCurrentProcess().Threads.Count);
}
}
}
By default, I see the following figures for the thread pool on my system:
{ maxWorkers = 32767, maxIOCPs = 1000, minWorkers = 4, minIOCPs = 4 }
Thread pool is lazy when growing threads. A new thread creation can be delayed for up to 500ms (for more details, check Joe Duffy's "CLR thread pool injection, stuttering problems").
To account for this behavior, use ThreadPool.SetMinThreads
. With SetMinThreads(100, 100)
, I see ~111 threads at peak for the sync version, and ~20 threads at peak for the async version (Release build, running without debugger). This is quite an indicative difference, on behalf of the async version.