Эффективно используйте async / await с веб-API ASP.NET

114

Я пытаюсь использовать async/awaitфункцию ASP.NET в своем проекте веб-API. Я не очень уверен, повлияет ли это на производительность моей службы веб-API. Ниже представлен рабочий процесс и пример кода из моего приложения.

Рабочий процесс:

Приложение пользовательского интерфейса → Конечная точка веб-API (контроллер) → Метод вызова на уровне службы веб-API → Вызов другой внешней веб-службы. (Здесь у нас есть взаимодействия с БД и т. Д.)

контроллер:

public async Task<IHttpActionResult> GetCountries()
{
    var allCountrys = await CountryDataService.ReturnAllCountries();

    if (allCountrys.Success)
    {
        return Ok(allCountrys.Domain);
    }

    return InternalServerError();
}

Уровень обслуживания:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");

    return Task.FromResult(response);
}

Я проверил приведенный выше код и работает. Но я не уверен, что это правильное использование async/await. Пожалуйста, поделитесь своими мыслями.

агр
источник

Ответы:

203

Я не очень уверен, повлияет ли это на производительность моего API.

Помните, что основное преимущество асинхронного кода на стороне сервера - это масштабируемость . Это не заставит ваши запросы работать быстрее. В статье об ASP.NET я рассмотрю несколько asyncвопросов, которые следует учитывать.async .

Я думаю, что ваш вариант использования (вызов других API) хорошо подходит для асинхронного кода, просто имейте в виду, что «асинхронный» не означает «быстрее». Лучший подход - сначала сделать ваш пользовательский интерфейс адаптивным и асинхронным; это заставит ваше приложение почувствовать быстрее , даже если это немного медленнее.

Что касается кода, это не асинхронно:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
  var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
  return Task.FromResult(response);
}

Вам потребуется действительно асинхронная реализация, чтобы получить преимущества масштабируемости async:

public async Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return await _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

Или (если ваша логика в этом методе действительно сквозная):

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

Учтите, что так легче работать «изнутри наружу», чем «снаружи внутрь». Другими словами, не начинайте с действия асинхронного контроллера, а затем заставляйте нижестоящие методы быть асинхронными. Вместо этого определите естественно асинхронные операции (вызов внешних API-интерфейсов, запросы к базе данных и т. Д.) И сделайте их асинхронными на самом низком уровне вначале ( Service.ProcessAsync). Тогда пустьasync потоку просочиться, сделав действия вашего контроллера асинхронными в качестве последнего шага.

И ни при каких обстоятельствах нельзя использовать Task.Runв этом сценарии.

Стивен Клири
источник
4
Спасибо Стивену за ценные комментарии. Я изменил все методы уровня обслуживания на асинхронные, и теперь я вызываю свой внешний вызов REST с помощью метода ExecuteTaskAsync, и он работает должным образом. Также спасибо за ваши сообщения в блоге об асинхронности и задачах. Это действительно очень помогло мне получить начальное понимание.
arp
5
Не могли бы вы объяснить, почему здесь нельзя использовать Task.Run?
Maarten
1
@Maarten: Я объясняю это (кратко) в статье, на которую я ссылался . Я подробно расскажу в своем блоге .
Стивен Клири
Я прочитал вашу статью, Стивен, и, кажется, что-то избегаю упоминания. Когда приходит запрос ASP.NET и он начинает выполняться в потоке пула потоков, это нормально. Но если он становится асинхронным, тогда да, он инициирует асинхронную обработку, и этот поток сразу же возвращается в пул. Но работа, выполняемая в методе async, сама будет выполняться в потоке пула потоков!
Хью
12

Это правильно, но, возможно, бесполезно.

Поскольку ждать нечего - нет вызовов API-интерфейсов блокировки, которые могут работать асинхронно - тогда вы настраиваете структуры для отслеживания асинхронной операции (которая имеет накладные расходы), но затем не используете эту возможность.

Например, если уровень сервиса выполнял операции БД с Entity Framework, который поддерживает асинхронные вызовы:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    using (db = myDBContext.Get()) {
      var list = await db.Countries.Where(condition).ToListAsync();

       return list;
    }
}

Вы позволите рабочему потоку делать что-то еще, пока запрашивается база данных (и, следовательно, может обрабатывать другой запрос).

Await, как правило, требует полного упразднения: очень сложно приспособиться к существующей системе.

Ричард
источник
-1

Вы не используете async / await эффективно, потому что поток запроса будет заблокирован при выполнении синхронного методаReturnAllCountries()

Поток, назначенный для обработки запроса, будет бездействовать, пока ReturnAllCountries() выполняет свою работу.

Если вы можете реализовать ReturnAllCountries()асинхронность, вы увидите преимущества масштабируемости. Это связано с тем, что поток может быть возвращен в пул потоков .NET для обработки другого запроса во время ReturnAllCountries()выполнения. Это позволит вашей службе иметь более высокую пропускную способность за счет более эффективного использования потоков.

Джеймс Вежба
источник
Это неправда. Сокет подключен, но поток, обрабатывающий запрос, может делать что-то еще, например обрабатывать другой запрос, ожидая вызова службы.
Гарр Годфри
-8

Я бы изменил ваш уровень обслуживания на:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    return Task.Run(() =>
    {
        return _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
    }      
}

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

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

Jonesopolis
источник
3
По ссылке в блоге я мог видеть, что даже если мы используем Task.Run, метод все равно работает синхронно?
arp
Хм. Между приведенным выше кодом и этой ссылкой есть много различий. Ваш Serviceне является асинхронным методом и не ожидается в самом Serviceвызове. Я могу понять его точку зрения, но не думаю, что она применима здесь.
Jonesopolis 02
8
Task.Runследует избегать в ASP.NET. Такой подход устранит все преимущества от async/ awaitи фактически будет работать хуже под нагрузкой, чем просто синхронный вызов.
Стивен Клири