Usa efficacemente async / await con l'API Web ASP.NET


114

Sto cercando di utilizzare la async/awaitfunzionalità di ASP.NET nel mio progetto API Web. Non sono molto sicuro se farà la differenza nelle prestazioni del mio servizio API Web. Di seguito trovi il flusso di lavoro e il codice di esempio dalla mia applicazione.

Flusso di lavoro:

Applicazione UI → Endpoint API Web (controller) → Chiama metodo nel livello di servizio API Web → Chiama un altro servizio Web esterno. (Qui abbiamo le interazioni DB, ecc.)

controller:

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

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

    return InternalServerError();
}

Livello di servizio:

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

    return Task.FromResult(response);
}

Ho testato il codice sopra e funziona. Ma non sono sicuro che sia l'uso corretto di async/await. Per favore condividi i tuoi pensieri.


Risposte:


202

Non sono molto sicuro se farà la differenza nelle prestazioni della mia API.

Tieni presente che il vantaggio principale del codice asincrono sul lato server è la scalabilità . Magicamente non renderà le tue richieste più veloci. Copro diverse considerazioni "dovrei usare async" nel mio articolo su asyncASP.NET .

Penso che il tuo caso d'uso (chiamare altre API) sia adatto per il codice asincrono, tieni presente che "asincrono" non significa "più veloce". L'approccio migliore è innanzitutto rendere la tua interfaccia utente reattiva e asincrona; questo renderà la vostra applicazione si sentono più veloce, anche se è leggermente più lento.

Per quanto riguarda il codice, questo non è asincrono:

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

Avresti bisogno di un'implementazione veramente asincrona per ottenere i vantaggi di scalabilità di async:

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

Oppure (se la tua logica in questo metodo è davvero solo un pass-through):

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

Nota che è più facile lavorare "dall'interno verso l'esterno" piuttosto che "dall'esterno verso l'interno" in questo modo. In altre parole, non iniziare con un'azione del controller asincrono e quindi forzare i metodi a valle ad essere asincroni. Identifica invece le operazioni naturalmente asincrone (chiamata ad API esterne, query di database, ecc.) E rendile asincrone al livello più basso first ( Service.ProcessAsync). Quindi lascia che il asyncflusso aumenti, rendendo le azioni del controller asincrone come l'ultimo passaggio.

E in nessuna circostanza dovresti usare Task.Runin questo scenario.


4
Grazie Stefano per il tuo prezioso commento. Ho modificato tutti i metodi del mio livello di servizio in modo che siano asincroni e ora chiamo la mia chiamata REST esterna utilizzando il metodo ExecuteTaskAsync e funziona come previsto. Grazie anche per i tuoi post sul blog riguardanti asincrono e Tasks. Mi ha davvero aiutato molto a ottenere una comprensione iniziale.
arp

5
Potresti spiegare perché non dovresti usare Task.Run qui?
Maarten

1
@ Maarten: lo spiego (brevemente) nell'articolo a cui ho linkato . Vado più in dettaglio sul mio blog .
Stephen Cleary

Ho letto il tuo articolo Stephen e sembra evitare di menzionare qualcosa. Quando una richiesta ASP.NET arriva e inizia, è in esecuzione su un thread del pool di thread, va bene. Ma se diventa asincrono, sì, avvia l'elaborazione asincrona e quel thread torna immediatamente nel pool. Ma il lavoro svolto nel metodo asincrono verrà eseguito esso stesso su un thread del pool di thread!
Hugh


12

È corretto, ma forse non utile.

Poiché non c'è nulla su cui attendere - nessuna chiamata alle API di blocco che potrebbero funzionare in modo asincrono - si stanno impostando strutture per tenere traccia dell'operazione asincrona (che ha un sovraccarico) ma quindi non si fa uso di tale capacità.

Ad esempio, se il livello di servizio stava eseguendo operazioni di database con Entity Framework che supporta chiamate asincrone:

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

       return list;
    }
}

Consentiresti al thread di lavoro di fare qualcos'altro mentre il database è stato interrogato (e quindi in grado di elaborare un'altra richiesta).

Await tende ad essere qualcosa che deve andare fino in fondo: è molto difficile adattarsi a un sistema esistente.


-1

Non stai sfruttando async / await in modo efficace perché il thread di richiesta verrà bloccato durante l'esecuzione del metodo sincronoReturnAllCountries()

Il thread assegnato per gestire una richiesta sarà in attesa pigramente mentre ReturnAllCountries()funziona.

Se puoi implementare ReturnAllCountries()in modo asincrono, vedrai vantaggi in termini di scalabilità. Ciò è perché il thread potrebbe essere rilasciato di nuovo al pool di thread .NET per gestire un'altra richiesta, durante l' ReturnAllCountries()esecuzione. Ciò consentirebbe al tuo servizio di avere un throughput più elevato, utilizzando i thread in modo più efficiente.


Questo non è vero. Il socket è connesso ma il thread che elabora la richiesta può fare qualcos'altro, come elaborare una richiesta diversa, mentre è in attesa di una chiamata di servizio.
Garr Godfrey

-8

Vorrei cambiare il tuo livello di servizio in:

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

dato che lo hai, stai ancora eseguendo la _service.Processchiamata in modo sincrono e ottenendo un vantaggio minimo o nullo dall'attesa.

Con questo approccio, si avvolge la chiamata potenzialmente lenta in una Task, la si avvia e la si restituisce in attesa. Ora hai il vantaggio di aspettare il file Task.


3
Come per il link del blog , ho potuto vedere che anche se usiamo Task.Run il metodo funziona ancora in modo sincrono?
arp

Hmm. Ci sono molte differenze tra il codice sopra e quel collegamento. Il tuo Servicenon è un metodo asincrono e non è atteso all'interno della Servicechiamata stessa. Posso capire il suo punto, ma non credo che si applichi qui.
Jonesopolis

8
Task.Rundovrebbe essere evitato su ASP.NET. Questo approccio eliminerebbe tutti i vantaggi da async/ awaite in realtà avrebbe prestazioni peggiori sotto carico rispetto a una semplice chiamata sincrona.
Stephen Cleary
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.