2013-12-12 13 views
13

tutaj na przepełnienie stosu mam found kod, który memoizes funkcje jednego argumentu:Jak wykonać funkcję zapamiętywania funkcji wątku w języku C#?

static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var d = new Dictionary<A, R>(); 
    return a=> 
    { 
     R r; 
     if (!d.TryGetValue(a, out r)) 
     { 
      r = f(a); 
      d.Add(a, r); 
     } 
     return r; 
    }; 
} 

Chociaż ten kod wykonuje swoją pracę dla mnie, nie jest on czasem gdy funkcja memoized jest wywoływana z wielu wątków jednocześnie: metoda Add zostaje wywołana dwukrotnie z tym samym argumentem i zgłasza wyjątek.

Jak wykonać zapamiętywanie wątku w bezpieczny sposób?

Odpowiedz

18

Można użyć ConcurrentDictionary.GetOrAdd który robi wszystko, czego potrzebujesz:

static Func<A, R> ThreadsafeMemoize<A, R>(this Func<A, R> f) 
{ 
    var cache = new ConcurrentDictionary<A, R>(); 

    return argument => cache.GetOrAdd(argument, f); 
} 

Funkcja f powinien być bezpieczny wątku sama, ponieważ może być wywołana z wielu wątków jednocześnie.

Ten kod nie gwarantuje również, że funkcja f zostanie wywołana tylko raz na unikalną wartość argumentu. Można go nazwać wiele razy, w rzeczywistości, w ruchliwym otoczeniu. Jeśli potrzebujesz tego rodzaju umowy, powinieneś spojrzeć na odpowiedzi w tym related question, ale należy pamiętać, że nie są one tak kompaktowe i wymagają użycia zamków.

+5

Zauważ, że 'GetOrAdd' nie zapobiega całkowicie wywołaniu f więcej niż jeden raz dla danego argumentu; gwarantuje jedynie, że wynik tylko * jeden * wywołań zostanie dodany do słownika. Można uzyskać więcej niż jedno wywołanie w przypadku jednoczesnego sprawdzania pamięci podręcznej wątków przed dodaniem wartości buforowanej. Często nie warto się tym martwić, ale wspominam o tym w przypadku, gdy wywołanie ma niepożądane skutki uboczne. –

+0

@JamesWorld Tak, zgadza się. Edytowana odpowiedź odzwierciedla to, dziękuję! – Gman

+1

Jestem nieco zdezorientowany - czy nie jest "cache" tutaj zmienną lokalną? Za każdym razem, gdy wywoływana jest 'ThreadsafeMemoize()', czy nie utworzy on nowego słownika? – dashnick

2

Tak jak Gman wspomniał o ConcurrentDictionary jest to preferowany sposób, ale jeśli nie jest to dostępne dla prostego oświadczenia lock wystarczy.

static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var d = new Dictionary<A, R>(); 
    return a=> 
    { 
     R r; 
     lock(d) 
     { 
      if (!d.TryGetValue(a, out r)) 
      { 
       r = f(a); 
       d.Add(a, r); 
      } 
     } 
     return r; 
    }; 
} 

jeden potencjalny problem przy użyciu zamki zamiast ConcurrentDictionary jest ta metoda może wprowadzać zakleszczenia się do swojego programu.

  1. Masz dwie memoized funkcje _memo1 = Func1.Memoize() i _memo2 = Func2.Memoize(), gdzie _memo1 i _memo2 są zmienne instancji.
  2. Wątki1 wywołań _memo1, Func1 rozpoczyna przetwarzanie.
  3. Wątek 2 dzwoni _memo2, wewnątrz Func2 jest wywołanie _memo1 i bloków Thread2.
  4. Przetwarzanie Thread1 z Func1 przychodzi do połączenia z _memo2 późno w funkcji bloków Thread1.
  5. DEADLOCK!

Więc jeśli w ogóle możliwe, należy używać ConcurrentDictionary, ale jeśli nie można i użyć zamiast blokad nie wywoływać inne funkcje, które są Memoized lunetą poza funkcją używasz w przypadku wewnątrz Memoized funkcji lub otwarciu aż do ryzyka zakleszczenia (jeśli _memo1 i _memo2 były zmiennymi lokalnymi zamiast zmiennymi instancji, impasu nie byłoby).

(Uwaga, wyniki mogą być nieco poprawić za pomocą ReaderWriterLock ale nadal będzie miał ten sam problem zakleszczenia.)

Powiązane problemy