Utiliser efficacement async / await avec l'API Web ASP.NET

114

J'essaye d'utiliser la async/awaitfonctionnalité d'ASP.NET dans mon projet d'API Web. Je ne sais pas si cela fera une différence dans les performances de mon service API Web. Veuillez trouver ci-dessous le workflow et un exemple de code de mon application.

Flux de travail:

Application UI → Point de terminaison API Web (contrôleur) → Méthode d'appel dans la couche de service API Web → Appel d'un autre service Web externe. (Ici, nous avons les interactions DB, etc.)

Manette:

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

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

    return InternalServerError();
}

Couche de service:

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

    return Task.FromResult(response);
}

J'ai testé le code ci-dessus et fonctionne. Mais je ne suis pas sûr que ce soit l'utilisation correcte de async/await. Veuillez partager vos pensées.

arp
la source

Réponses:

202

Je ne suis pas sûr que cela fasse une différence dans les performances de mon API.

Gardez à l'esprit que le principal avantage du code asynchrone côté serveur est l' évolutivité . Cela ne rendra pas vos demandes plus rapides comme par magie. Je couvre plusieurs considérations "devrais-je utiliser async" dans mon article sur asyncASP.NET .

Je pense que votre cas d'utilisation (appeler d'autres API) est bien adapté pour le code asynchrone, gardez simplement à l'esprit que «asynchrone» ne signifie pas «plus rapide». La meilleure approche consiste d'abord à rendre votre interface utilisateur réactive et asynchrone; cela rendra votre application sensation plus vite , même si elle est un peu plus lent.

En ce qui concerne le code, ce n'est pas asynchrone:

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

Vous auriez besoin d'une implémentation véritablement asynchrone pour bénéficier des avantages d'évolutivité de async:

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

Ou (si votre logique dans cette méthode est vraiment juste un pass-through):

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

Notez qu'il est plus facile de travailler de "l'intérieur vers l'extérieur" plutôt que de "l'extérieur vers l'intérieur" comme ceci. En d'autres termes, ne commencez pas par une action de contrôleur asynchrone, puis ne forcez pas les méthodes en aval à être asynchrones. Au lieu de cela, identifiez les opérations naturellement asynchrones (appel d'API externes, requêtes de base de données, etc.) et rendez-les asynchrones au niveau le plus bas en premier ( Service.ProcessAsync). Ensuite, laissez couler l' asynceau, rendant les actions de votre contrôleur asynchrones lors de la dernière étape.

Et en aucun cas vous ne devez utiliser Task.Rundans ce scénario.

Stephen Cleary
la source
4
Merci Stephen pour vos précieux commentaires. J'ai changé toutes mes méthodes de couche de service pour qu'elles soient asynchrones et maintenant j'appelle mon appel REST externe à l'aide de la méthode ExecuteTaskAsync et fonctionne comme prévu. Merci également pour vos articles de blog concernant l'async et les tâches. Cela m'a vraiment beaucoup aidé à avoir une première compréhension.
arp
5
Pourriez-vous expliquer pourquoi vous ne devriez pas utiliser Task.Run ici?
Maarten
1
@Maarten: Je l'explique (brièvement) dans l'article auquel j'ai lié . J'entre plus en détail sur mon blog .
Stephen Cleary
J'ai lu votre article Stephen et il semble éviter de mentionner quelque chose. Lorsqu'une demande ASP.NET arrive et commence, elle s'exécute sur un thread de pool de threads, c'est très bien. Mais s'il devient asynchrone, alors oui, il lance le traitement asynchrone et ce thread revient immédiatement dans le pool. Mais le travail effectué dans la méthode async s'exécutera lui-même sur un thread de pool de threads!
Hugh
12

C'est correct, mais peut-être pas utile.

Comme il n'y a rien à attendre - pas d'appels aux API de blocage qui pourraient fonctionner de manière asynchrone - alors vous configurez des structures pour suivre le fonctionnement asynchrone (qui a une surcharge), mais vous n'utilisez pas cette capacité.

Par exemple, si la couche de service effectuait des opérations de base de données avec Entity Framework qui prend en charge les appels asynchrones:

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

       return list;
    }
}

Vous autoriseriez le thread de travail à faire autre chose pendant que la base de données était interrogée (et donc capable de traiter une autre demande).

Attendre a tendance à être quelque chose qui doit aller jusqu'au bout: il est très difficile de s'intégrer à un système existant.

Richard
la source
-1

Vous n'utilisez pas efficacement async / await car le thread de demande sera bloqué lors de l'exécution de la méthode synchroneReturnAllCountries()

Le thread qui est assigné pour gérer une demande attendra pendant ReturnAllCountries()qu'il fonctionne.

Si vous pouvez implémenter ReturnAllCountries()pour être asynchrone, alors vous verriez des avantages d'évolutivité. Cela est dû au fait que le thread peut être relâché dans le pool de threads .NET pour gérer une autre demande, pendant ReturnAllCountries()son exécution. Cela permettrait à votre service d'avoir un débit plus élevé, en utilisant plus efficacement les threads.

James Wierzba
la source
Ce n'est pas vrai. Le socket est connecté mais le thread traitant la demande peut faire autre chose, comme traiter une demande différente, en attendant un appel de service.
Garr Godfrey
-8

Je changerais votre couche de service en:

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

comme vous l'avez, vous exécutez toujours votre _service.Processappel de manière synchrone et ne tirez que très peu ou pas d'avantages à l'attendre.

Avec cette approche, vous encapsulez l'appel potentiellement lent dans un Task, le démarrez et le renvoyez en attente. Vous avez maintenant l'avantage d'attendre le fichier Task.

Jonesopolis
la source
3
Selon le lien du blog , je pourrais voir que même si nous utilisons Task.Run, la méthode fonctionne toujours de manière synchrone?
arp
Hmm. Il existe de nombreuses différences entre le code ci-dessus et ce lien. Votre Servicen'est pas une méthode asynchrone et n'est pas attendue dans l' Serviceappel lui-même. Je peux comprendre son argument, mais je ne pense pas qu'il s'applique ici.
Jonesopolis
8
Task.Rundoit être évité sur ASP.NET. Cette approche supprimerait tous les avantages de async/ awaitet serait en fait pire sous charge qu'un simple appel synchrone.
Stephen Cleary