Avvolgere il codice sincrono in una chiamata asincrona


95

Ho un metodo nell'applicazione ASP.NET, che richiede molto tempo per essere completato. Una chiamata a questo metodo potrebbe verificarsi fino a 3 volte durante una richiesta utente, a seconda dello stato della cache e dei parametri forniti dall'utente. Ogni chiamata richiede circa 1-2 secondi per essere completata. Il metodo stesso è una chiamata sincrona al servizio e non è possibile sovrascrivere l'implementazione.
Quindi la chiamata sincrona al servizio è simile alla seguente:

public OutputModel Calculate(InputModel input)
{
    // do some stuff
    return Service.LongRunningCall(input);
}

E l'utilizzo del metodo è (nota, quella chiamata di metodo può accadere più di una volta):

private void MakeRequest()
{
    // a lot of other stuff: preparing requests, sending/processing other requests, etc.
    var myOutput = Calculate(myInput);
    // stuff again
}

Ho provato a cambiare l'implementazione da parte mia per fornire il lavoro simultaneo di questo metodo, ed ecco cosa sono arrivato finora.

public async Task<OutputModel> CalculateAsync(InputModel input)
{
    return await Task.Run(() =>
    {
        return Calculate(input);
    });
}

Utilizzo (parte del codice "fai altre cose" viene eseguita contemporaneamente alla chiamata al servizio):

private async Task MakeRequest()
{
    // do some stuff
    var task = CalculateAsync(myInput);
    // do other stuff
    var myOutput = await task;
    // some more stuff
}

La mia domanda è la seguente. Utilizzo l'approccio corretto per accelerare l'esecuzione nell'applicazione ASP.NET o sto eseguendo un lavoro non necessario cercando di eseguire codice sincrono in modo asincrono? Qualcuno può spiegare perché il secondo approccio non è un'opzione in ASP.NET (se non lo è davvero)? Inoltre, se tale approccio è applicabile, devo chiamare tale metodo in modo asincrono se è l'unica chiamata che potremmo eseguire al momento (ho questo caso, quando non c'è altra cosa da fare in attesa del completamento)?
La maggior parte degli articoli in rete su questo argomento tratta dell'utilizzo async-awaitdell'approccio con il codice, che già fornisce awaitablemetodi, ma non è il mio caso. Quiè il bell'articolo che descrive il mio caso, che non descrive la situazione delle chiamate parallele, rifiutando l'opzione di wrap sync call, ma secondo me la mia situazione è proprio l'occasione per farlo.
Grazie in anticipo per aiuto e suggerimenti.

Risposte:


116

È importante fare una distinzione tra due diversi tipi di concorrenza. La concorrenza asincrona si verifica quando sono presenti più operazioni asincrone in volo (e poiché ogni operazione è asincrona, nessuna di esse utilizza effettivamente un thread ). La concorrenza parallela si verifica quando più thread eseguono ciascuna un'operazione separata.

La prima cosa da fare è rivalutare questo assunto:

Il metodo stesso è una chiamata sincrona al servizio e non è possibile sovrascrivere l'implementazione.

Se il tuo "servizio" è un servizio web o qualsiasi altra cosa legata a I / O, la soluzione migliore è scrivere per esso un'API asincrona.

Procederò partendo dal presupposto che il tuo "servizio" sia un'operazione legata alla CPU che deve essere eseguita sulla stessa macchina del server web.

In tal caso, la prossima cosa da valutare è un'altra ipotesi:

Ho bisogno che la richiesta venga eseguita più velocemente.

Sei assolutamente sicuro che sia quello che devi fare? È possibile apportare modifiche al front-end, ad esempio avviare la richiesta e consentire all'utente di eseguire altre operazioni durante l'elaborazione?

Procederò partendo dal presupposto che sì, è davvero necessario che la richiesta individuale venga eseguita più velocemente.

In questo caso, dovrai eseguire codice parallelo sul tuo server web. Questo è decisamente sconsigliato in generale perché il codice parallelo utilizzerà thread di cui ASP.NET potrebbe aver bisogno per gestire altre richieste e rimuovendo / aggiungendo thread disattiverà l'euristica del pool di thread ASP.NET. Quindi, questa decisione ha un impatto sull'intero server.

Quando usi codice parallelo su ASP.NET, stai prendendo la decisione di limitare realmente la scalabilità della tua app web. Potresti anche vedere una discreta quantità di thread churn, soprattutto se le tue richieste sono a raffica. Consiglio di usare solo codice parallelo su ASP.NET se sai che il numero di utenti simultanei sarà piuttosto basso (cioè, non un server pubblico).

Quindi, se arrivi a questo punto e sei sicuro di voler eseguire l'elaborazione parallela su ASP.NET, hai un paio di opzioni.

Uno dei metodi più semplici è quello di utilizzare Task.Run, molto simile al codice esistente. Tuttavia, non consiglio di implementare un CalculateAsyncmetodo poiché ciò implica che l'elaborazione è asincrona (cosa che non è). Usa invece Task.Runal momento della chiamata:

private async Task MakeRequest()
{
  // do some stuff
  var task = Task.Run(() => Calculate(myInput));
  // do other stuff
  var myOutput = await task;
  // some more stuff
}

In alternativa, se funziona bene con il tuo codice, è possibile utilizzare il Paralleltipo, vale a dire, Parallel.For, Parallel.ForEach, o Parallel.Invoke. Il vantaggio del Parallelcodice è che il thread di richiesta viene utilizzato come uno dei thread paralleli e quindi riprende l'esecuzione nel contesto del thread (c'è meno cambio di contesto rispetto asyncall'esempio):

private void MakeRequest()
{
  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));
}

Non consiglio affatto di usare Parallel LINQ (PLINQ) su ASP.NET.


1
Grazie mille per una risposta dettagliata. Un'altra cosa che voglio chiarire. Quando inizio l'esecuzione utilizzando Task.Run, il contenuto viene eseguito in un altro thread, che viene preso dal pool di thread. Quindi non è affatto necessario avvolgere le chiamate di blocco (come la mia chiamata al servizio) in Runs, perché consumeranno sempre un thread ciascuno, che verrà bloccato durante l'esecuzione del metodo. In una situazione del genere l'unico vantaggio che rimane async-awaitnel mio caso è eseguire più azioni alla volta. Per favore correggimi se sbaglio.
Eadel

2
Sì, l'unico vantaggio che ottieni da Run(o Parallel) è la concorrenza. Le operazioni stanno ancora bloccando ciascuna un thread. Poiché dici che il servizio è un servizio web, non consiglio di utilizzare Runo Parallel; scrivi invece un'API asincrona per il servizio.
Stephen Cleary

3
@StephenCleary. cosa intendi per "... e poiché ogni operazione è asincrona, nessuna di esse utilizza effettivamente un thread ..." grazie!
alltej

3
@alltej: per un codice veramente asincrono, non c'è thread .
Stephen Cleary,

4
@JoshuaFrank: Non direttamente. Il codice dietro un'API sincrona deve bloccare il thread chiamante, per definizione. È possibile utilizzare un'API asincrona come BeginGetResponsee iniziare a molti di coloro, e poi (sincrono) Blocco fino a quando tutti sono completi, ma questo tipo di approccio è difficile - vedi blog.stephencleary.com/2012/07/dont-block-on -async-code.html e msdn.microsoft.com/en-us/magazine/mt238404.aspx . Di solito è più facile e più pulito adottare asynccompletamente, se possibile.
Stephen Cleary

1

Ho scoperto che il codice seguente può convertire un'attività in modo che venga sempre eseguita in modo asincrono

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)
{
    await Task.Yield();
    return await func();
}

e l'ho usato nel modo seguente

await ForceAsync(() => AsyncTaskWithNoAwaits())

Questo eseguirà qualsiasi attività in modo asincrono in modo da poterle combinare negli scenari WhenAll, WhenAny e altri usi.

Puoi anche aggiungere semplicemente Task.Yield () come prima riga del codice chiamato.

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.