2015-06-03 13 views
7

Ich versuche, die folgende Methode (vereinfachtes Beispiel) zu konvertieren asynchron zu sein, da die cacheMissResolver Aufruf in Bezug auf die Zeit (Datenbanksuche, Netzanruf) teuer sein kann:Korrekter Weg, Methode in C# in async zu konvertieren?

// 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; 
    } 
} 

Es gibt viele Materialien Online über asynchrone Methoden zu konsumieren, aber die Ratschläge, die ich bei der Herstellung gefunden habe, scheinen weniger klar zu sein. In Anbetracht dessen, dass dies Teil einer Bibliothek sein soll, ist einer meiner Versuche unten korrekt?

// 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; 
    } 
} 

Nutzt SemaphoreSlim die richtige Art und Weise eine Sperre Anweisung in einem Asynchron-Verfahren zu ersetzen? (Ich kann im Körper einer Lock-Anweisung nicht erwarten.)

Sollte ich stattdessen das cacheMissResolver Argument vom Typ Func<Task<Thing>> machen? Obwohl dies die Last auf sich zieht sicherzustellen, dass die Resolver-Funktion asynchron für den Aufrufer ist (Wrapping in Task.Run, weiß ich, dass es in einen Hintergrundthread ausgelagert wird, wenn es lange dauert).

Danke.

+1

Verwenden Sie [AsyncLock'] (https://github.com/StephenCleary/AsyncEx/wiki/AsyncLock). –

+0

Was ist 'Cache'? – dbc

+0

@Timothy Shields - Das sieht nützlich aus! Vielen Dank. – rob

Antwort

3

Verwendet SemaphoreSlim den richtigen Weg, um eine Lock-Anweisung in einer asynchronen Methode zu ersetzen?

Ja.

Soll ich stattdessen das cacheMissResolver Argument vom Typ Func<Task<Thing>> machen?

Ja. Es wird dem Aufrufer ermöglichen, einen inhärent asynchronen Vorgang (z. B. IO) bereitzustellen, anstatt dies nur für lang laufende Arbeit geeignet CPU gebunden arbeiten. (Während noch CPU gebunden Arbeit zu unterstützen, indem Sie einfach mit dem Anrufer Task.Run nutzen, um sich, wenn das, was sie tun wollen.)


Other than that, beachten Sie nur, dass es nicht in Point mit await Task.FromResult(...); Wrapping einen Wert in einem Task nur um es sofort auszupacken ist es sinnlos. Verwenden Sie das Ergebnis direkt in solchen Situationen, in diesem Fall geben Sie den zwischengespeicherten Wert direkt zurück. Was Sie tun, ist nicht wirklich falsch, es ist nur unnötig komplizieren/verwirren den Code.

+0

Danke. Ich hatte die Implikationen des Verwendens auf Task.FromResult (...) nicht verstanden. – rob

3

Wenn Ihr Cache-Speicher ist (es sieht aus wie es ist), dann die Aufgaben Cachen betrachten anstatt die Ergebnisse. Dies hat eine nette Seiteneigenschaft, wenn zwei Methoden den gleichen Schlüssel anfordern, nur eine einzige Auflösungsanforderung. Da nur der Cache gesperrt ist (und nicht die auflösenden Operationen), können Sie weiterhin eine einfache Sperre verwenden.

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); 
    } 
    } 
} 

Dies wird jedoch auch Ausnahmen zwischenspeichern, die Sie nicht möchten. Eine Möglichkeit, dies zu vermeiden, ist die Ausnahme Aufgabe zu ermöglichen, den Cache zunächst eingeben, aber dann ist es zu beschneiden, wenn der nächste Antrag gestellt wird:

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); 
    } 
    } 
} 

können Sie entscheiden, diese zusätzliche Prüfung ist nicht erforderlich, wenn Sie einen anderen Prozess haben Beschneiden der Cache regelmäßig.

+0

Danke dafür. Ich hatte nicht daran gedacht, die Aufgaben selbst zu cachen. Prost auch für das Schreiben so viel zu dem Thema, fand eine Reihe von Ihren Artikeln nützlich beim Lernen mehr! – rob

Verwandte Themen