为什么我的TPL程序比其async / await对应程序使用更多的ThreadPool资源?

时间:2014-04-07 04:09:53

标签: .net io task-parallel-library async-await threadpool

我正在编写一个程序,演示在服务器可伸缩性的上下文中使用异步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;
}

2 个答案:

答案 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.Currentawait使用与当前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?"