Защо TPL програмата използва повече ThreadPool ресурси от нейния аналог с async/await?

+5 гласа
53 прегледа
попитан 2016 март 31 в .NET от Nikoleta.V. (4,090 точки)
етикетиран отново 2016 март 31 от Nikoleta.V.

Пиша програма,която показва предимствата на асинхронния 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).

Изпълнението е каквото се очаква и намирам само 2 или 3 различни ID-та на нишки, когато разглеждам разултата. Това е нормално за моята машина,тъй като има само 2 ядра и не може да се разпределят повече нишки от броя на ядрата.

Сега опитвам да направя същия анализ, но с TPL функция, която не използва await. Тя изглежда така:

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(пак на същата двуядрена машина).

Защо го има това несъответствие? Разбирам че await не е незначителен за използването на Task<T>, но пък броя на нишките е общия знаменател и очаквам същото преизползване на нишките в TPL имплементацията.

Има ли друг начин да се напише TPL методът, за да се постигнат същите резултати без заключване?

Добавка:

SleepAsync е асинхронният метод, генериран от WCF за за следната синхронна операция. Забележете, че в този случай клиентът не блокира,докато сървърът го прави.

public TimeSpan Sleep(TimeSpan time)

{

    Thread.Sleep(time);

    return time;

}

1 отговор

+1 глас
отговорени 2016 март 31 от valeri.hristov (7,340 точки)
избран 2016 март 31 от Admin
 
Най-добър отговор

В този случай, класическата 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();

 }

От друга страна, продълженията с await обикновено се изпълняват синхронно(ако операцияа завърши на същия синхронизационен контекс, на който е започната или ако няма синхронизация в двете точки на изпълнение). Затова се очаква да придобие с 2 нишки по-малко.

Добро четиво по темата е :

http://blogs.msdn.com/b/pfxteam/archive/2010/05/24/10014209.aspx

...