我正在编写一个程序,演示在服务器可伸缩性的上下文中使用异步IO的好处。程序同时使用异步方法,然后报告参与异步处理的线程的ID。
为了说明,请考虑以下事项:
static async Task<TimeSpan> AsyncCalling(TimeSpan time)
{
using (SleepService.SleepServiceClient client = new SleepService.SleepServiceClient())
{
TimeSpan response = await client.SleepAsync(time);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
response += await client.SleepAsync(TimeSpan.FromTicks(time.Ticks / 2));
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
return response;
}
}
我通过调用上面的异步方法模拟负载下的服务器,如下所示:
int numberOfWorkItems = 50;
for (int i = 0; i < numberOfWorkItems; ++i)
{
TimeSpan value = TimeSpan.FromSeconds((i % 3) + 1);
ThreadPool.QueueUserWorkItem(arg => { TimeSpan t = AsyncCalling(value).Result; });
Thread.Sleep(300);
}
ThreadPool.QueueUserWorkItem
操作模拟请求线程的分配,AsyncCalling
方法是请求执行的方法(类似于WCF的操作)。
执行是按预期进行的,在分析输出时我只计算两到三个不同的线程ID。这对我的机器来说是典型的,因为我只有两个内核,并且线程池将阻止调度比可用内核更多的线程。
现在我尝试进行相同的分析,但对于没有使用await
关键字的TPL功能。功能如下:
static Task<TimeSpan> TaskAsyncCalling(TimeSpan time)
{
SleepService.SleepServiceClient client = new SleepService.SleepServiceClient();
return client.SleepAsync(time)
.ContinueWith(t =>
{
TimeSpan result = t.Result;
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
return client.SleepAsync(TimeSpan.FromTicks(time.Ticks / 2))
.ContinueWith(t1 =>
{
result += t1.Result;
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
(client as IDisposable).Dispose();
return result;
});
})
.Unwrap();
}
在同一上下文中调用TaskAsyncCalling
时,输出结果完全不同。执行任务通常需要更长的时间,并且唯一线程ID的总数通常为30(对于我的2核机器)。
为什么存在这种差异?我理解await
不是Task<T>
的简单包装器,但是线程池是公共分母,我期望在TPL实现中重复使用相同的聪明线程。
是否有另一种方法可以重写TPL方法以实现相同的结果而不会阻塞?
编辑:
SleepAsync
调用是用于以下同步操作的异步生成的WCF客户端方法。请注意,在这种情况下,客户端不会阻止服务器的位置。
public TimeSpan Sleep(TimeSpan time)
{
Thread.Sleep(time);
return time;
}
答案 0 :(得分:1)
我不确定这两个实现是否相同,await
存储当前
SyncrhonizationContext
并获取与之关联的TaskScheduler。这不是ContinueWith
的默认实现。来自反射器:
public Task ContinueWith(Action<Task<TResult>> continuationAction)
{
StackCrawlMark lookForMyCaller = StackCrawlMark.LookForMyCaller;
return this.ContinueWith(continuationAction, TaskScheduler.Current, new CancellationToken(), TaskContinuationOptions.None, ref lookForMyCaller);
}
因此ContinueWith
使用TaskScheduler.Current
而await
使用与当前TaskScheduler
相关联的SyncrhonizationContext
。如果两者不一样,你可能会得到不同的行为。
尝试为TaskScheduler.FromCurrentSynchronizationContext()
指定ContinueWith
,看看是否有任何差异。
答案 1 :(得分:1)
在这种情况下,经典TPL版本使用的线程数多于async/await
版本,因为每个ContinueWith
延续都在单独的池线程上执行。
使用TaskContinuationsOptions.ExecuteSynchronously
修正:
static Task<TimeSpan> TaskAsyncCalling(TimeSpan time)
{
SleepService.SleepServiceClient client = new SleepService.SleepServiceClient();
return client.SleepAsync(time)
.ContinueWith(t =>
{
TimeSpan result = t.Result;
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
return client.SleepAsync(TimeSpan.FromTicks(time.Ticks / 2))
.ContinueWith(t1 =>
{
result += t1.Result;
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
(client as IDisposable).Dispose();
return result;
}, TaskContinuationsOptions.ExecuteSynchronously);
}, TaskContinuationsOptions.ExecuteSynchronously)
.Unwrap();
}
OTOH,await
延续通常是同步执行的(如果操作在启动的同一上下文中完成,或者在两个执行点都没有同步)。因此预计会减少两个线程。
一个很好的相关阅读:"Why is TaskContinuationsOptions.ExecuteSynchronously opt-in?"