J'essaie de comparer (à l'aide du banc Apache) quelques points de terminaison ASP.NET Web API 2.0. L'un est synchrone et l'autre asynchrone.
[Route("user/{userId}/feeds")]
[HttpGet]
public IEnumerable<NewsFeedItem> GetNewsFeedItemsForUser(string userId)
{
return _newsFeedService.GetNewsFeedItemsForUser(userId);
}
[Route("user/{userId}/feeds/async")]
[HttpGet]
public async Task<IEnumerable<NewsFeedItem>> GetNewsFeedItemsForUserAsync(string userId)
{
return await Task.Run(() => _newsFeedService.GetNewsFeedItemsForUser(userId));
}
Après avoir regardé présentation de Steve Sanderson J'ai émis la commande suivante ab -n 100 -c 10 http://localhost....
à chaque noeud final.
J'ai été surpris car les repères pour chaque point final semblaient être approximativement les mêmes.
En partant de ce que Steve a expliqué, je m'attendais à ce que le point de terminaison asynchrone soit plus performant car il libérerait immédiatement les threads du pool de threads dans le pool de threads, les rendant ainsi disponibles pour d'autres demandes et améliorant le débit. Mais les chiffres semblent exactement les mêmes.
Qu'est-ce que je comprends mal ici?
En utilisant await Task.Run
pour créer "async" WebApi est une mauvaise idée - vous utiliserez toujours un thread, et même à partir de le même pool de threads utilisé pour les demandes .
Cela conduira à des moments désagréables décrits en détails ici :
- Changement de thread supplémentaire (inutile) vers le thread du pool de threads Task.Run. De même, lorsque ce thread termine la demande, il doit entrer le contexte de la demande (qui n'est pas un commutateur de thread réel mais qui a une surcharge).
- Des déchets supplémentaires (inutiles) sont créés. La programmation asynchrone est un compromis: vous obtenez une réactivité accrue au détriment d'une utilisation accrue de la mémoire. Dans ce cas, vous finissez par créer plus de déchets pour les opérations asynchrones, ce qui est totalement inutile.
- Les heuristiques du pool de threads ASP.NET sont supprimées par Task.Run empruntant "de manière inattendue" un thread de pool de threads. Je n'ai pas beaucoup d'expérience ici, mais mon instinct me dit que l'heuristique devrait bien récupérer si la tâche inattendue est vraiment courte et ne la gérerait pas aussi élégamment si la tâche inattendue dure plus de deux secondes.
- ASP.NET n'est pas en mesure de mettre fin à la demande plus tôt, c'est-à-dire si le client se déconnecte ou si la demande expire. Dans le cas synchrone, ASP.NET connaissait le thread de demande et pouvait l'annuler. Dans le cas asynchrone, ASP.NET n'est pas conscient que le thread du pool de threads secondaire est "pour" cette demande. Il est possible de résoudre ce problème en utilisant des jetons d'annulation, mais cela n'entre pas dans le cadre de cet article de blog.
Fondamentalement, vous n'autorisez aucune asynchronie vers ASP.NET - vous masquez simplement le code synchrone lié au CPU derrière la façade asynchrone. Async
en lui-même est idéal pour le code lié aux E/S, car il permet d'utiliser le processeur (threads) à son efficacité maximale (pas de blocage pour les E/S), mais lorsque vous avez du code lié au calcul, vous devra toujours utiliser le CPU dans la même mesure.
Et en tenant compte de la surcharge supplémentaire de Task
et du changement de contexte, vous obtiendrez des résultats encore plus mauvais qu'avec les méthodes de contrôleur de synchronisation simples.
COMMENT LE RENDRE VRAIMENT ASYNC:
La méthode GetNewsFeedItemsForUser
doit être transformée en async
.
[Route("user/{userId}/feeds/async")]
[HttpGet]
public async Task<IEnumerable<NewsFeedItem>> GetNewsFeedItemsForUserAsync(string userId)
{
return await _newsFeedService.GetNewsFeedItemsForUser(userId);
}
Pour le faire:
async
(s'il n'y en a pas - pas de chance, vous devrez rechercher un analogue concurrent).