我有一个简单的HTTP Server实现。代码如下所示。它在具有32个核心的服务器计算机上进行了测试。如果我将processContext方法包装到Task.Run调用中,那么性能会加倍(至少)。考虑到这给了我在这个特定情况下的性能提升,我现在很困惑:有一些方法返回一个我不想等待的任务,我应该遵循什么策略?我应该直接调用它还是应该在Task.Run中包装?
class Program
{
private static ConcurrentBag<DateTime> _trails = new ConcurrentBag<DateTime>();
static void Main(string[] args)
{
Console.WriteLine($"Is server GC: {(GCSettings.IsServerGC ? "true" : "false")}");
string prefix = args[0];
CancellationTokenSource cancellationSource = new CancellationTokenSource();
HttpListener httpListener = new HttpListener();
httpListener.Prefixes.Add(prefix);
httpListener.Start();
Task.Run(async () =>
{
while (!cancellationSource.Token.IsCancellationRequested)
{
HttpListenerContext context = null;
try
{
context = await httpListener.GetContextAsync();
if (cancellationSource.Token.IsCancellationRequested)
{
context.Response.Abort();
break;
}
}
catch (ObjectDisposedException)
{
return;
}
catch (HttpListenerException ex)
{
if (cancellationSource.Token.IsCancellationRequested && ex.ErrorCode == 995)
{
break;
}
throw;
}
// Uncommenting below line and commenting the next one improves the performance at least twice
// Task childProcessingTask = Task.Run(async () => await processContext(context));
var dt = processContext(context);
}
});
using (Timer t = new Timer(o => Console.Title = $"Async Server: {_trails.Count}", null, 0, 5000))
{
Console.WriteLine("Running...");
Console.ReadLine();
cancellationSource.Cancel();
Console.WriteLine("Stopped accepting new request. Waiting for pending requests...");
Console.WriteLine("Stopped");
httpListener.Close();
}
var gTrails = _trails.GroupBy(t => new DateTime(t.Year, t.Month, t.Day, t.Hour, t.Minute, 0))
.Select(g => new { MinuteDt = g.Key, Count = g.Count() })
.OrderBy(x => x.MinuteDt).ToList();
gTrails.ForEach(x => Console.WriteLine($"{x.MinuteDt:HH:mm}\t{x.Count}"));
if (gTrails.Count > 2)
{
decimal avg = gTrails.Take(gTrails.Count - 1).Skip(1).Average(g => (decimal)g.Count);
Console.WriteLine($"Average: {avg:0.00}/min, {avg / 60.0m:00.0}/sec");
}
Console.ReadLine();
}
private static async Task processContext(HttpListenerContext context)
{
DateTime requestDt = DateTime.Now;
Stopwatch sw = Stopwatch.StartNew();
string requestId = context.Request.QueryString["requestId"];
byte[] requestIdBytes = Encoding.ASCII.GetBytes(requestId);
context.Response.ContentLength64 = requestIdBytes.Length;
await context.Response.OutputStream.WriteAsync(requestIdBytes, 0, requestIdBytes.Length);
try
{
context.Response.Close();
}
catch { }
_trails.Add(requestDt);
}
}
答案 0 :(得分:1)
为什么将等待的异步方法包装到Task.Run中会至少提高两次性能?
许多异步方法都有一个同步部分,它在调用者的线程上同步运行。
对于processContext
方法,从开始到第一个await的该方法的代码在调用者的线程上运行。
如果您不使用Task.Run
,则在接受连接后,您的软件首先运行processContext方法的同步部分。它调用await,任务的上下文进入堆,线程自由并恢复while (!cancellationSource.Token.IsCancellationRequested)
循环的另一次迭代。
很快任务就完成了,调度程序想要恢复它。
但它无法在它开始的同一个线程上恢复它,因为该线程可能正在忙于侦听新连接并启动另一个子任务。
你有很多核心,任务将在另一个核心上恢复非常好的变化。如果它发生在同一CPU的另一个核心上,则核心必须等待来自L3缓存的数据(如局部变量,HttpListenerContext实例变量等),因为L1和L2缓存是每个核心。如果它发生在另一个CPU上,核心将不得不等待系统RAM,这甚至更慢。
如果您使用Task.Run
,则运行该无限while(!IsCancellationRequested)
循环的线程会继续执行此操作,并立即继续执行该循环的另一次迭代,所有数据都已打开该核心的缓存。
processContext
方法将从一开始就在其他核心上运行。如果您只发送几个字节,则await WriteAsync
将以非常快的速度返回。调度程序并不愚蠢。如果您有32个核心且没有那么多任务,则调度程序可能会在启动它的同一核心上恢复processContext
任务,并且所有特定于会话的数据已经存在于该核心的缓存中。
我应该遵循什么策略?
测试几种技术,选择适合您工作负载的更好的方法。 Task.Run并不总是更快,只是在你的情况下。
或者,了解事情是如何运作的,你将能够做出有根据的猜测。实施起来不太精确,但要快得多。