2015-06-03 2 views
7

Sto tentando di convertire il metodo seguente (esempio semplificato) di essere asincrona, come la chiamata cacheMissResolver può essere costoso in termini di tempo (ricerca nel database, chiamata di rete):Modo corretto per convertire il metodo in asincrono in C#?

// Synchronous version 
public class ThingCache 
{ 
    private static readonly object _lockObj; 
    // ... other stuff 

    public Thing Get(string key, Func<Thing> cacheMissResolver) 
    { 
     if (cache.Contains(key)) 
      return cache[key]; 

     Thing item; 

     lock(_lockObj) 
     { 
      if (cache.Contains(key)) 
       return cache[key]; 

      item = cacheMissResolver();  
      cache.Add(key, item); 
     } 

     return item; 
    } 
} 

Ci sono un sacco di materiali on-line sul consumo di metodi asincroni, ma il consiglio che ho trovato nel produrli sembra meno chiaro. Dato che questo è destinato a far parte di una biblioteca, uno dei miei tentativi è corretto?

// Asynchronous attempts 
public class ThingCache 
{ 
    private static readonly SemaphoreSlim _lockObj = new SemaphoreSlim(1); 
    // ... other stuff 

    // attempt #1 
    public async Task<Thing> Get(string key, Func<Thing> cacheMissResolver) 
    { 
     if (cache.Contains(key)) 
      return await Task.FromResult(cache[key]); 

     Thing item; 

     await _lockObj.WaitAsync(); 

     try 
     { 
      if (cache.Contains(key)) 
       return await Task.FromResult(cache[key]); 

      item = await Task.Run(cacheMissResolver).ConfigureAwait(false); 
      _cache.Add(key, item); 
     } 
     finally 
     { 
      _lockObj.Release(); 
     } 

     return item; 
    } 

    // attempt #2 
    public async Task<Thing> Get(string key, Func<Task<Thing>> cacheMissResolver) 
    { 
     if (cache.Contains(key)) 
      return await Task.FromResult(cache[key]); 

     Thing item; 

     await _lockObj.WaitAsync(); 

     try 
     { 
      if (cache.Contains(key)) 
       return await Task.FromResult(cache[key]); 

      item = await cacheMissResolver().ConfigureAwait(false); 
      _cache.Add(key, item); 
     } 
     finally 
     { 
      _lockObj.Release(); 
     } 

     return item; 
    } 
} 

Sta usando SemaphoreSlim il modo corretto per sostituire una dichiarazione di blocco in un metodo asincrono? (Non posso attendere nel corpo di una dichiarazione di blocco.)

Devo inserire l'argomento cacheMissResolver di tipo Func<Task<Thing>>? Anche se ciò pone l'onere di assicurarsi che la funzione resolver sia asincrona sul chiamante (eseguendo il wrapping in Task.Run, so che verrà scaricato su un thread in background se impiega molto tempo).

Grazie.

+1

Considerare l'utilizzo di ['AsyncLock'] (https://github.com/StephenCleary/AsyncEx/wiki/AsyncLock). –

+0

Che cos'è 'cache'? – dbc

+0

@Timothy Shields - Sembra utile! Grazie. – rob

risposta

3

Sta usando SemaphoreSlim il modo corretto di sostituire un'istruzione di blocco in un metodo asincrono?

Sì.

Devo inserire l'argomento cacheMissResolver di tipo Func<Task<Thing>>?

Sì. Consentirà al chiamante di fornire un'operazione intrinsecamente asincrona (come IO) piuttosto che renderlo adatto solo per il lavoro che è in esecuzione da tempo CPU limitata lavoro. (Mentre ancora sostenere CPU bound lavoro semplicemente avendo usare il chiamante Task.Run stessi, se è questo che vogliono fare.)


Oltre a questo, basta notare che non ci sta senso avere await Task.FromResult(...); Wrapping un valore in un Task solo per scartarlo immediatamente è inutile. Basta usare il risultato direttamente in tali situazioni, in questo caso, restituire direttamente il valore memorizzato nella cache. Quello che stai facendo non è in realtà errato, è solo inutilmente complicare/confondere il codice.

+0

Grazie. Non avevo colto le implicazioni dell'uso dell'attesa su Task.FromResult (...). – rob

3

Se la cache è in memoria (sembra che sia), quindi prendere in considerazione la memorizzazione nella cache i compiti piuttosto che i risultati . Questo ha una bella proprietà laterale se due metodi richiedono la stessa chiave, solo una singola richiesta di risoluzione viene presa. Inoltre, poiché solo la cache è bloccata (e non le operazioni di risoluzione), puoi continuare a utilizzare un semplice blocco.

public class ThingCache 
{ 
    private static readonly object _lockObj; 

    public async Task<Thing> GetAsync(string key, Func<Task<Thing>> cacheMissResolver) 
    { 
    lock (_lockObj) 
    { 
     if (cache.Contains(key)) 
     return cache[key]; 
     var task = cacheMissResolver(); 
     _cache.Add(key, task); 
    } 
    } 
} 

Tuttavia, questo memorizzerà anche le eccezioni nella cache, che potresti non volere. Un modo per evitare questo è di permettere il compito eccezione per entrare nella cache inizialmente, ma poi potare quando viene fatta la richiesta successiva:

public class ThingCache 
{ 
    private static readonly object _lockObj; 

    public async Task<Thing> GetAsync(string key, Func<Task<Thing>> cacheMissResolver) 
    { 
    lock (_lockObj) 
    { 
     if (cache.Contains(key)) 
     { 
     if (cache[key].Status == TaskStatus.RanToCompletion) 
      return cache[key]; 
     cache.Remove(key); 
     } 
     var task = cacheMissResolver(); 
     _cache.Add(key, task); 
    } 
    } 
} 

si può decidere questo controllo supplementare è necessario se si dispone di un altro processo potatura della cache periodicamente.

+0

Grazie per quello. Non avevo pensato di mettere in cache gli stessi compiti. Applausi anche per aver scritto così tanto sull'argomento, ho trovato alcuni dei tuoi articoli utili per saperne di più! – rob