Come evitare di violare il principio DRY quando è necessario disporre sia della versione asincrona che della sincronizzazione del codice?


15

Sto lavorando a un progetto che deve supportare sia la versione asincrona che quella sincronizzata di una stessa logica / metodo. Quindi, per esempio, devo avere:

public class Foo
{
   public bool IsIt()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return conn.Query<bool>("SELECT IsIt FROM SomeTable");
      }
   }

   public async Task<bool> IsItAsync()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return await conn.QueryAsync<bool>("SELECT IsIt FROM SomeTable");
      }
   }
}

La logica asincrona e di sincronizzazione di questi metodi sono identici sotto tutti gli aspetti tranne che uno è asincrono e un altro no. Esiste un modo legittimo per evitare di violare il principio DRY in questo tipo di scenario? Ho visto che la gente dice che potresti usare GetAwaiter (). GetResult () su un metodo asincrono e invocarlo dal tuo metodo di sincronizzazione? Quel thread è sicuro in tutti gli scenari? C'è un altro modo migliore per farlo o sono costretto a duplicare la logica?


1
Puoi dire qualcosa in più su quello che stai facendo qui? In particolare, come stai raggiungendo l'asincronia nel tuo metodo asincrono? Il lavoro ad alta latenza è associato alla CPU o all'IO?
Eric Lippert,

"uno è asincrono e un altro no" è la differenza nel codice del metodo effettivo o solo l'interfaccia? (Chiaramente solo per l'interfaccia è possibile return Task.FromResult(IsIt());)
Alexei Levenkov

Inoltre, presumibilmente le versioni sincrone e asincrone sono entrambe ad alta latenza. In quali circostanze il chiamante utilizzerà la versione sincrona e in che modo quel chiamante mitigherà il fatto che la chiamata potrebbe richiedere miliardi di nanosecondi? Il chiamante della versione sincrona non si preoccupa di appendere l'interfaccia utente? Non c'è un'interfaccia utente? Raccontaci di più sullo scenario.
Eric Lippert,

@EricLippert Ho aggiunto un esempio fittizio specifico solo per darti un'idea di come 2 basi di codici sono identiche oltre al fatto che uno è asincrono. Puoi facilmente immaginare uno scenario in cui questo metodo è molto più complesso e le righe e le righe di codice devono essere duplicate.
Marko

@AlexeiLevenkov La differenza è davvero nel codice, ho aggiunto del codice fittizio per dimostrarlo.
Marko

Risposte:


15

Hai fatto diverse domande nella tua domanda. Li analizzerò in modo leggermente diverso rispetto a te. Ma prima lasciami rispondere direttamente alla domanda.

Vogliamo tutti una fotocamera che sia leggera, di alta qualità ed economica, ma come dice il proverbio, puoi ottenere solo un massimo di due su quei tre. Sei nella stessa situazione qui. Volete una soluzione che sia efficiente, sicura e condivida il codice tra i percorsi sincroni e asincroni. Ne otterrai solo due.

Lasciami analizzare perché. Inizieremo con questa domanda:


Ho visto che la gente dice che potresti usare GetAwaiter().GetResult() un metodo asincrono e invocarlo dal tuo metodo di sincronizzazione? Quel thread è sicuro in tutti gli scenari?

Il punto di questa domanda è "posso condividere i percorsi sincroni e asincroni facendo in modo che il percorso sincrono faccia semplicemente un'attesa sincrona sulla versione asincrona?"

Vorrei essere super chiaro su questo punto perché è importante:

DOVREI FERMARE IMMEDIATAMENTE DI PRENDERE QUALSIASI CONSIGLIO DA QUESTE PERSONE .

Questo è un consiglio estremamente negativo. È molto pericoloso recuperare un risultato in modo sincrono da un'attività asincrona a meno che non si abbia la prova che l'attività è stata completata normalmente o in modo anomalo .

Il motivo per cui questo è un consiglio estremamente negativo è, beh, considerare questo scenario. Volete falciare il prato, ma la lama del tosaerba è rotta. Decidi di seguire questo flusso di lavoro:

  • Ordina un nuovo blade da un sito web. Questa è un'operazione asincrona a latenza elevata.
  • Attendi in modo sincrono, ovvero dormi fino a quando non hai il blade in mano .
  • Controllare periodicamente la cassetta postale per vedere se il blade è arrivato.
  • Rimuovere la lama dalla scatola. Ora ce l'hai in mano.
  • Installare la lama nel tosaerba.
  • Falciare il prato.

Che succede? Dormi per sempre perché l'operazione di controllo della posta è ora bloccata su qualcosa che accade dopo l'arrivo della posta .

È estremamente facile entrare in questa situazione quando si attende in modo sincrono un'attività arbitraria. Quell'attività potrebbe aver programmato il lavoro in futuro del thread che è ora in attesa , e ora quel futuro non arriverà mai perché lo stai aspettando.

Se fai un'attesa asincrona, allora va tutto bene! Controlli periodicamente la posta e, mentre aspetti, fai un sandwich o fai le tue tasse o altro; continui a lavorare mentre aspetti.

Non attendere mai in modo sincrono. Se l'attività viene eseguita, non è necessario . Se l'attività non viene eseguita ma pianificata per l'esecuzione del thread corrente, è inefficiente perché il thread corrente potrebbe servire altri lavori invece di attendere. Se l'attività non è fatto ed eseguire programma sul thread corrente, è appeso ad aspettare in modo sincrono. Non vi è alcuna buona ragione per attendere in modo sincrono, di nuovo, a meno che non si sappia già che l'attività è completa .

Per ulteriori informazioni su questo argomento, vedere

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

Stephen spiega lo scenario del mondo reale molto meglio di me.


Consideriamo ora "l'altra direzione". Possiamo condividere il codice rendendo la versione asincrona semplicemente fare la versione sincrona su un thread di lavoro?

Questa è probabilmente e probabilmente una cattiva idea, per i seguenti motivi.

  • È inefficiente se l'operazione sincrona è un lavoro IO ad alta latenza. Questo essenzialmente assume un lavoratore e lo fa dormire fino a quando non viene eseguita un'attività. Le discussioni sono follemente costose . Consumano un milione di byte di spazio minimo per impostazione predefinita, richiedono tempo, prendono risorse del sistema operativo; non vuoi bruciare un thread facendo un lavoro inutile.

  • L'operazione sincrona potrebbe non essere scritta come thread-safe.

  • Questa è una tecnica più ragionevole se il lavoro ad alta latenza è associato al processore, ma in tal caso probabilmente non si desidera semplicemente passarlo a un thread di lavoro. Probabilmente si desidera utilizzare la libreria di attività parallele per parallelizzare il maggior numero possibile di CPU, probabilmente si desidera la logica di annullamento e non si può semplicemente fare in modo che la versione sincrona faccia tutto ciò, perché sarebbe già la versione asincrona .

Ulteriori letture; ancora una volta, Stephen lo spiega molto chiaramente:

Perché non usare Task.Run:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html

Altri scenari "fai e non fare" per Task.Run:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html


Cosa ci lascia quindi? Entrambe le tecniche per la condivisione del codice portano a deadlock o grandi inefficienze. La conclusione che giungiamo è che devi fare una scelta. Vuoi un programma che sia efficiente e corretto e che rallegri il chiamante o vuoi salvare alcune sequenze di tasti comportando la duplicazione di una piccola quantità di codice tra i percorsi sincrono e asincrono? Non hai entrambe le cose, temo.


Puoi spiegare perché tornare Task.Run(() => SynchronousMethod())nella versione asincrona non è quello che vuole l'OP?
Guillaume Sasdy,

2
@GuillaumeSasdy: il poster originale ha risposto alla domanda su cosa sia il lavoro: è legato all'IO. È inutile eseguire attività legate a IO su un thread di lavoro!
Eric Lippert

Va bene lo capisco. Penso che tu abbia ragione, e anche la risposta di @StriplingWarrior lo spiega ancora di più.
Guillaume Sasdy,

2
@Marko quasi ogni soluzione alternativa che blocca il thread alla fine (sotto carico elevato) consumerà tutti i thread del pool di thread (che di solito venivano utilizzati per completare le operazioni asincrone). Di conseguenza, tutti i thread saranno in attesa e nessuno sarà disponibile per consentire alle operazioni di eseguire "operazioni asincrone completate" parte del codice. La maggior parte delle soluzioni alternative va bene in scenari a basso carico (poiché ci sono molti thread anche 2-3 sono bloccati come parte di ogni singola operazione) ... E se puoi garantire che il completamento dell'operazione asincrona venga eseguito sul nuovo thread del sistema operativo (non nel pool di thread), potrebbe anche funzionare per tutti i casi (paghi un prezzo così alto)
Alexei Levenkov

2
A volte non c'è altro modo che "semplicemente fare la versione sincrona su un thread di lavoro". Ad esempio, è così che Dns.GetHostEntryAsyncviene implementato in .NET e FileStream.ReadAsyncper alcuni tipi di file. Il sistema operativo non fornisce semplicemente un'interfaccia asincrona, quindi il runtime deve falsificarlo (e non è specifico della lingua; ad esempio, il runtime di Erlang esegue l'intero albero dei processi di lavoro , con più thread all'interno di ciascuno, per fornire I / O e nome del disco non bloccanti risoluzione).
Joker_vD

6

È difficile dare una risposta unica per tutti. Sfortunatamente non esiste un modo semplice e perfetto per riutilizzare tra codice asincrono e sincrono. Ma qui ci sono alcuni principi da considerare:

  1. Il codice asincrono e sincrono è spesso fondamentalmente diverso. Il codice asincrono dovrebbe in genere includere un token di annullamento, ad esempio. E spesso finisce per chiamare diversi metodi (come il tuo esempio chiama Query()in uno e QueryAsync()nell'altro) o impostare connessioni con impostazioni diverse. Quindi, anche quando è strutturalmente simile, spesso ci sono abbastanza differenze nel comportamento da meritare che vengano trattati come codice separato con requisiti diversi. Notare le differenze tra le implementazioni di metodi Async e Sync nella classe File, ad esempio: non viene fatto alcuno sforzo per farle usare lo stesso codice
  2. Se stai fornendo una firma del metodo asincrono per il bene di implementare un'interfaccia, ma ti capita di avere un'implementazione sincrona (cioè non c'è nulla di intrinsecamente asincrono su ciò che fa il tuo metodo), puoi semplicemente restituire Task.FromResult(...) .
  3. Qualsiasi pezzo di logica sincrono che è lo stesso tra i due metodi può essere estratto in un metodo di supporto separato e sfruttato in entrambi i metodi.

In bocca al lupo.


-2

Facile; fare in modo che quello sincrono chiami quello asincrono. C'è anche un metodo utile Task<T>per fare proprio questo:

public class Foo
{
   public bool IsIt()
   {
      var task = IsItAsync(); //no await here; we want the Task

      //Some tasks end up scheduled to run before you get them;
      //don't try to run them a second time
      if((int)task.Status > (int)TaskStatus.Created)
          //this call will block the current thread,
          //and unlike Run()/Wait() will prefer the current 
          //thread's TaskScheduler instead of a new thread.
          task.RunSynchronously(); 

      //if IsItAsync() can throw exceptions,
      //you still need a Wait() call to bring those back from the Task
      try{ 
          task.Wait();
          return task.Result;
      }
      catch(Exception ex) 
      { 
          //Handle IsItAsync() exceptions here;
          //remember to return something if you don't rethrow              
      }
   }

   public async Task<bool> IsItAsync()
   {
      // Some async logic
   }
}

1
Vedi la mia risposta sul perché questa è la peggiore pratica che non devi mai fare.
Eric Lippert

1
La tua risposta riguarda GetAwaiter (). GetResult (). L'ho evitato. La realtà è che a volte è necessario eseguire un metodo in modo sincrono per utilizzarlo in uno stack di chiamate che non può essere reso asincrono. Se l'attesa di sincronizzazione non dovrebbe mai essere eseguita, quindi come (ex) membro del team C #, per favore dimmi perché hai inserito non solo due modi di attendere in modo sincrono su un'attività asincrona nel TPL.
KeithS

La persona a porre quella domanda sarebbe Stephen Toub, non io. Ho lavorato solo sul lato linguistico delle cose.
Eric Lippert
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.