ASP.NET С# 5 Асинхронные веб-приложения с использованием Async & Await

Изучив концепцию асинхронной веб-разработки, в частности из этого источника, я создал пример приложения для доказательства концепции.

Решение состоит из двух приложений ASP.NET Web API. Первая - имитированная медленная конечная точка; он ждет 1000 мс, прежде чем возвращать список пользовательскому классу Student:

 public IEnumerable<Student> Get()
    {
        Thread.Sleep(1000);
        return new List<Student> { new Student { Name = @"Paul" }, new Student { Name = @"Steve" }, new Student { Name = @"Dave" }, new Student { Name = @"Sue" } };
    }

Вот класс ученика:

public class Student
{
    public string Name { get; set; }
}

Эта конечная точка размещена в IIS 7 на localhost: 4002.

Второе приложение связывается с первым, используя 2 конечных точки, один синхронный, другой асинхронный:

public IEnumerable<Student> Get() {
        var proxy = WebRequest.Create(@"http://localhost:4002/api/values");

        var response = proxy.GetResponse();
        var reader = new StreamReader(response.GetResponseStream());

        return JsonConvert.DeserializeObject<IEnumerable<Student>>(reader.ReadToEnd());
    }

    public async Task<IEnumerable<Student>> Get(int id) {
        var proxy = new HttpClient();
        var getStudents = proxy.GetStreamAsync(@"http://localhost:4002/api/values");

        var stream = await getStudents;
        var reader = new StreamReader(stream);

        return JsonConvert.DeserializeObject<IEnumerable<Student>>(reader.ReadToEnd());
    }

Он размещен в IIS 7 на localhost: 4001.

Обе конечные точки работают, как ожидалось, и возвращаются в ок. 1 секунда. Основываясь на видео в ссылке выше в 13:25, асинхронный метод должен освободить его Thread, сводя к минимуму конкуренцию.

Я запускаю тесты производительности приложения с помощью Apache Bench. Вот время отклика синхронного метода с 10 одновременными запросами:

Synchronous Results

Это так, как я ожидал; более параллельные соединения увеличивают конфликт и продлевают время отклика. Однако здесь есть время асинхронного ответа:

Asynchronous Results

Как вы можете видеть, все еще кажется, что некоторые споры. Я ожидал, что среднее время отклика будет более сбалансированным. Если я запускаю тесты на обеих конечных точках с 50 одновременными запросами, я все равно получаю аналогичные результаты.

На основании этого кажется, что оба асинхронных и синхронных метода работают с большей или меньшей скоростью (ожидается), не принимая во внимание накладные расходы в асинхронных методах, но также и то, что асинхронный метод, похоже, не является освобождая потоки обратно в ThreadPool. Я бы приветствовал любые комментарии или пояснения, спасибо.

Ответ 1

Я думаю, что есть неплохие шансы, что вы не тестируете то, что, по вашему мнению, испытываете. Из того, что я могу собрать, вы пытаетесь обнаружить выпуски обратно в пул потоков, сравнивая тайминги и вызывая инъекцию потоков.

С одной стороны, настройки по умолчанию для пула потоков на .NET 4.5 чрезвычайно высоки. Вы не будете ударять их только с 10 или 100 одновременными запросами.

Повторите шаг назад и подумайте о том, что вы хотите проверить: метод async возвращает поток в пул потоков?

У меня есть демоверсия, которую я показываю, чтобы продемонстрировать это. Я не хотел создавать тестовую нагрузку для моей демонстрации (работает на моем ноутбуке для презентаций), поэтому я сделал небольшой трюк: я искусственно ограничил пул потоков более разумным значением.

Как только вы это сделаете, ваш тест достаточно прост: выполните много одновременных подключений, а затем выполните так много плюс один. Синхронная реализация должна будет дождаться завершения до начала последнего, в то время как асинхронная реализация сможет запустить их все.

На стороне сервера сначала ограничьте потоки пула потоков количеством процессоров в системе:

protected void Application_Start()
{
    int workerThreads, ioThreads;
    ThreadPool.GetMaxThreads(out workerThreads, out ioThreads);
    ThreadPool.SetMaxThreads(Environment.ProcessorCount, ioThreads);
    ...
}

Затем выполняются синхронные и асинхронные реализации:

public class ValuesController : ApiController
{
    // Synchronous
    public IEnumerable<string> Get()
    {
        Thread.Sleep(1000);
        return new string[] { "value1", "value2" };
    }

    // Asynchronous
    public async Task<IEnumerable<string>> Get(int id)
    {
        await Task.Delay(1000);
        return new string[] { "value1", "value2" };
    }
}

И, наконец, клиентский тестовый код:

static void Main(string[] args)
{
    try
    {
        MainAsync().Wait();
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }

    Console.ReadKey();
}

static async Task MainAsync()
{
    ServicePointManager.DefaultConnectionLimit = int.MaxValue;

    var sw = new Stopwatch();
    var client = new HttpClient();
    var connections = Environment.ProcessorCount;
    var url = "http://localhost:35697/api/values/";

    await client.GetStringAsync(url); // warmup
    sw.Start();
    await Task.WhenAll(Enumerable.Range(0, connections).Select(i => client.GetStringAsync(url)));
    sw.Stop();
    Console.WriteLine("Synchronous time for " + connections + " connections: " + sw.Elapsed);

    connections = Environment.ProcessorCount + 1;

    await client.GetStringAsync(url); // warmup
    sw.Restart();
    await Task.WhenAll(Enumerable.Range(0, connections).Select(i => client.GetStringAsync(url)));
    sw.Stop();
    Console.WriteLine("Synchronous time for " + connections + " connections: " + sw.Elapsed);

    url += "13";
    connections = Environment.ProcessorCount;

    await client.GetStringAsync(url); // warmup
    sw.Restart();
    await Task.WhenAll(Enumerable.Range(0, connections).Select(i => client.GetStringAsync(url)));
    sw.Stop();
    Console.WriteLine("Asynchronous time for " + connections + " connections: " + sw.Elapsed);

    connections = Environment.ProcessorCount + 1;

    await client.GetStringAsync(url); // warmup
    sw.Restart();
    await Task.WhenAll(Enumerable.Range(0, connections).Select(i => client.GetStringAsync(url)));
    sw.Stop();
    Console.WriteLine("Asynchronous time for " + connections + " connections: " + sw.Elapsed);
}

На моем (8-логическом ядре) машине я вижу вывод следующим образом:

Synchronous time for 8 connections: 00:00:01.0194025
Synchronous time for 9 connections: 00:00:02.0362007
Asynchronous time for 8 connections: 00:00:01.0413737
Asynchronous time for 9 connections: 00:00:01.0238674

Что ясно показывает, что асинхронный метод возвращает поток в пул потоков.