2009-08-10 21 views
21

के प्रारंभिक बिंदु के रूप में कार्य करने Memoization Wes Dyer's दृष्टिकोण लेते हैं:थ्रेड-सुरक्षित Memoization

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

समस्या है, जब यह एक से अधिक थ्रेड से उपयोग कर रहा है, हम मुसीबत में पड़ सकते हैं:

Func<int, int> f = ... 
var f1 = f.Memoize(); 
... 
in thread 1: 
var y1 = f1(1); 
in thread 2: 
var y2 = f1(1); 
// We may be recalculating f(1) here! 

आइए इससे बचने की कोशिश करें। map पर लॉक करना:

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

स्पष्ट रूप से एक भयानक विचार है क्योंकि यह हमें एक बार में कई अलग तर्क पर f1 की गणना करने से रोकता है है। a पर लॉकिंग काम नहीं करेगा यदि a में एक मान प्रकार है (और किसी भी दर पर एक बुरा विचार है, क्योंकि हम a को नियंत्रित नहीं करते हैं और बाहरी कोड भी इसे लॉक कर सकता है)।

यहाँ दो विकल्प हैं मैं के बारे में सोच सकते हैं:

आलसी मूल्यांकन के लिए एक Lazy<T> वर्ग मानते हुए (here देखें):

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

या तुल्यकालन के लिए वस्तुओं की एक अतिरिक्त शब्दकोश रखते हुए:

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var map = new Dictionary<A, R>(); 
    var mapSync = new Dictionary<A, object>(); 
    return a => 
    { 
     R value; 
     object sync; 
     lock(mapSync) 
     { 
     if (!mapSync.TryGetValue(a, out sync)) 
     { 
      sync = new object(); 
      mapSync[a] = sync; 
     } 
     } 
     lock(map) 
     { 
     if (map.TryGetValue(a, out value)) 
      return value; 
     } 
     lock(sync) 
     { 
     value = f(a); 
     lock(map) 
     { 
      map[a] = value; 
     } 
     return value; 
     } 
    }; 
} 

कोई बेहतर विकल्प?

उत्तर

33

अनावश्यक Lazy<R> के बिना .NET 4.0 के ConcurrentDictionary<A, R> का उपयोग करें।
कुंजी GetOrAdd(A, Func<A, R>) है जो एक खूबसूरती से छोटे लैम्ब्डा में प्रस्तुत करता है।

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var cache = new ConcurrentDictionary<A, R>(); 
    return a => cache.GetOrAdd(a, f); 
}; 

अद्यतन ऊपर समाधान एकाधिक पाठकों भूमि के ऊपर कम से कम के साथ & लेखकों की अनुमति नहीं देता। लेकिन, यह f(a) को उसी मान के लिए एक से अधिक बार निष्पादित करने से नहीं रोकता है (अवधि के दौरान इसकी गणना की जा रही है)।

यदि यह आपके लिए महत्वपूर्ण है, तो आप Lazy<R> में मान लपेट सकते हैं लेकिन आपको प्रत्येक पढ़ने के लिए लागत लगती है। नियमित Dictionary रूप में एक ही - - लेकिन 720msLazy संस्करण के लिए

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var cache = new ConcurrentDictionary<A, Lazy<R>>(); 
    return a => cache.GetOrAdd(a, new Lazy<R>(() => f(a))).Value; 
} 

एक लाख के लिए अद्यतन समय परीक्षण एक पूर्व आबादी वाले 1000-आइटम कैश शो 19ms ConcurrentDictionary के लिए की पढ़ता है।

यदि यह बहुत खड़ा लगता है, तो आप दोनों जटिलताओं के साथ एक और जटिल समाधान के साथ सर्वश्रेष्ठ प्राप्त कर सकते हैं।

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var cache = new ConcurrentDictionary<A, R>(); 
    var syncMap = new ConcurrentDictionary<A, object>(); 
    return a => 
    { 
     R r; 
     if (!cache.TryGetValue(a, out r)) 
     { 
      var sync = syncMap.GetOrAdd(a, new object()); 
      lock (sync) 
      { 
       r = cache.GetOrAdd(a, f); 
      } 
      syncMap.TryRemove(a, out sync); 
     } 
     return r; 
    }; 
} 
+2

मैं यह कहना चाहूंगा कि यह एक उत्कृष्ट उत्तर है। धन्यवाद! –

1

नहीं, वे बेहतर विकल्प नहीं हैं।

आलसी मूल्यांकन वाला संस्करण व्यर्थ है क्योंकि आप इसे तुरंत मूल्यांकन करते हैं। सिंक्रनाइज़ेशन डिक्शनरी वाला संस्करण ठीक से काम नहीं करता है क्योंकि आप इसका उपयोग करने से पहले लॉक के अंदर मैप डिक्शनरी की रक्षा नहीं कर रहे हैं।

संस्करण जिसे आपने भयानक कहा है वास्तव में सबसे अच्छा विकल्प है। आपको मानचित्र लॉक को लॉक के अंदर सुरक्षित रखना होगा ताकि एक समय में केवल एक थ्रेड इसे एक्सेस कर सके। शब्दकोश थ्रेड सुरक्षित नहीं है, इसलिए यदि आप इसे एक थ्रेड पढ़ते हैं, जबकि एक और धागा इसे बदल रहा है, तो आपको समस्याएं होंगी।

याद रखें कि मानचित्र ऑब्जेक्ट पर लॉक का उपयोग करके मैप ऑब्जेक्ट को स्वयं में सुरक्षित नहीं किया जाता है, यह केवल लॉक के अंदर कोड चलाने के लिए एक से अधिक थ्रेड रखने के लिए पहचानकर्ता के रूप में मानचित्र संदर्भ का उपयोग कर रहा है। आपको ऑब्जेक्ट को बदलने वाले कोड को न केवल लॉक के अंदर ऑब्जेक्ट तक पहुंचने वाले सभी कोड डालना होगा।

+0

मैंने आलसी मूल्यांकन संस्करण तय कर दिया है। –

+0

और सिंक्रनाइज़ेशन शब्दकोश संस्करण। –

+0

आलसी मूल्यांकन संस्करण अभी भी बिंदु है क्योंकि मूल्य हमेशा मूल्यांकन किया जाता है। सिंक्रनाइज़ेशन शब्दकोश संस्करण अभी भी सुरक्षित नहीं है, क्योंकि विभिन्न धागे एक ही कुंजी के लिए ऑब्जेक्ट बना सकते हैं, और एक दूसरे को ओवरराइट कर देगा। – Guffa

10

आप पहले से ही है कि Lazy<T> प्रकार, मैं तुम्हें .net 4.0 का उपयोग कर रहे हैं, ताकि आप भी इस्तेमाल कर सकते हैं मान है, तो ConcurrentDictionary<A,R>:

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var map = new ConcurrentDictionary<A, Lazy<R>>(); 
    return a => 
    { 
     Lazy<R> lazy = new Lazy<R>(() => f(a), LazyExecutionMode.EnsureSingleThreadSafeExecution); 
     if(!map.TryAdd(a, lazy)) 
     { 
     return map[a].Value; 
     } 
     return lazy.Value; 
    }; 
} 
0

आप पढ़ी comment from Dyer थ्रेड-सुरक्षित करने के लिए लेख में संबंधित ?

शायद मेमोइज़ थ्रेड-सुरक्षित बनाने का सबसे आसान तरीका मानचित्र पर लॉक डालना है।

यह सुनिश्चित करेगा कि ज्ञापन किया जा रहा कार्य केवल अलग तर्कों के प्रत्येक सेट के लिए एक बार चलाया जाएगा।

RoboRally गेम के मेरे उदाहरण में, मैंने वास्तव में "सरोगेट सिंगलटन" के रूप में कार्य करने के लिए फ़ंक्शन ज्ञापन का उपयोग किया।यह वास्तव में एक सिंगलटन नहीं है क्योंकि प्रति कारखाना उदाहरण एक उदाहरण हो सकता है (जब तक कि कारखाना स्थैतिक न हो)। लेकिन यह वही है जो मैं चाहता था।

+0

हां, यह _easiest_ तरीका है। मैंने विशेष रूप से कहा कि इसके बारे में क्या बुरा है: यह हमें अलग-अलग तर्कों पर कार्य का मूल्यांकन करने से रोकता है। –

1

आप एक ही मूल्य की गणना दो बार नहीं करना चाहते हैं और आप चाहते हैं कि कई थ्रेड मूल्यों की गणना करने में सक्षम हों और समवर्ती रूप से मूल्यों को पुनर्प्राप्त करें। ऐसा करने के लिए आपको कुछ प्रकार की हालत परिवर्तनीय और अच्छी तरह से दाग लॉकिंग सिस्टम का उपयोग करने की आवश्यकता होगी।

विचार है। जब कोई मान मौजूद नहीं होता है तो आप सिंक मैप में एक मान डालते हैं और फिर उस थ्रेड को उस मूल्य की आवश्यकता होती है जो इसके लिए प्रतीक्षा करेगी अन्यथा आप वर्तमान मूल्य को पकड़ लेंगे। इस तरह नक्शे के लॉकिंग को मानों के लिए पूछताछ और मूल्यों को वापस करने के लिए कम किया जाता है।

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
    { 
     var map = new Dictionary<A, R>(); 
     var mapSync = new Dictionary<A, object>(); 
     return a => 
     { 
      R value; 
      object sync = null; 
      bool calc = false; 
      bool wait = false; 
      lock (map) 
      { 
       if (!map.TryGetValue(a, out value)) 
       { 
        //its not in the map 
        if (!mapSync.TryGetValue(a, out sync)) 
        { 
         //not currently being created 
         sync = new object(); 
         mapSync[a] = sync; 
         calc = true; 

        } 
        else 
        { 
         calc = false; 
         wait = true; 
        } 
       } 
      } 
      if(calc) 
      { 
       lock (sync) 
       { 
        value = f(a); 
        lock (map) 
        { 
         map.Add(a, value); 
         mapSync.Remove(a); 
        } 
        Monitor.PulseAll(sync); 
        return value; 
       } 
      } 
      else if (wait) 
      { 
       lock (sync) 
       { 
        while (!map.TryGetValue(a, out value)) 
        { 
         Monitor.Wait(sync); 
        } 
        return value; 
       } 
      } 

      lock (map) 
      { 
       return map[a]; 
      } 

     }; 
    } 

यह केवल एक त्वरित पहला प्रयास है, लेकिन मुझे लगता है कि यह तकनीक का प्रदर्शन करता है। यहां आप गति के लिए अतिरिक्त मेमोरी का व्यापार कर रहे हैं।

2

थॉमस का जवाब आलसी कन्स्ट्रक्टर के लिए एनम पैरामीटर के कारण .NET 4.0 के तहत संकलित प्रतीत नहीं होता है। मैंने इसे नीचे संशोधित किया। मैंने अपनी खुद की समानता तुलनाकर्ता की आपूर्ति के लिए एक वैकल्पिक पैरामीटर भी जोड़ा। यह उपयोगी है अगर TInput अपने स्वयं के बराबर लागू नहीं करता है या यदि TInput एक स्ट्रिंग है और आप इसे केस असंवेदनशील बनाना चाहते हैं, उदाहरण के लिए।

public static Func<TInput, TResult> Memoize<TInput, TResult>(
     this Func<TInput, TResult> func, IEqualityComparer<TInput> comparer = null) 
    { 
     var map = comparer == null 
         ? new ConcurrentDictionary<TInput, Lazy<TResult>>() 
         : new ConcurrentDictionary<TInput, Lazy<TResult>>(comparer); 

     return input => 
       { 
        var lazy = new Lazy<TResult>(() => func(input), LazyThreadSafetyMode.ExecutionAndPublication); 

        return map.TryAdd(input, lazy) 
           ? lazy.Value 
           : map[input].Value; 
       }; 
    } 

मैं अपने परीक्षण के रूप में इस का उपयोग करते हुए इस विधि के कुछ बुनियादी परीक्षण किया:

public void TestMemoize() 
    { 
     Func<int, string> mainFunc = i => 
            { 
             Console.WriteLine("Evaluating " + i); 
             Thread.Sleep(1000); 
             return i.ToString(); 
            }; 

     var memoized = mainFunc.Memoize(); 

     Parallel.ForEach(
      Enumerable.Range(0, 10), 
      i => Parallel.ForEach(Enumerable.Range(0, 10), j => Console.WriteLine(memoized(i)))); 
    } 

यह सही ढंग से काम कर रहा है।

0

निगेल Touch की उत्कृष्ट जवाब पर विस्तार, मैं एक पुन: प्रयोज्य घटक मंगलाचरण च के लिए गिनती सीमित उनके समाधान से निकाला पेशकश करने के लिए करना चाहता था (क)।

मैं इसे SynchronizedConcurrentDictionary कहा जाता है, और यह इस तरह दिखता है:

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var cache = new SynchronizedConcurrentDictionary<A, R>(); 

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

चीयर्स:

public class SynchronizedConcurrentDictionary<TKey, TValue> : ConcurrentDictionary<TKey, TValue> 
{ 
    private readonly ReaderWriterLockSlim _cacheLock = new ReaderWriterLockSlim(); 

    public new TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory) 
    { 
     TValue result; 

     _cacheLock.EnterWriteLock(); 
     try 
     { 
      result = base.GetOrAdd(key, valueFactory); 
     } 
     finally 
     { 
      _cacheLock.ExitWriteLock(); 
     } 

     return result; 
    } 
} 

फिर Memoize समारोह एक दो लाइनर बन जाता है!

+0

कोई टिप्पणी के साथ डाउनवोट क्यों? मैं बस कुछ हासिल करने की कोशिश कर रहा था और समुदाय के लिए उपयोगी पाया। समस्या क्या है? –

+0

नोट: नाम "सिंक्रनाइज़ किया गया ConcurrentDictionary" शायद एक बुरा है! ConcurrentDictionary आईसीओलेक्शन लागू करता है, जिसमें एक संपत्ति "IsSynchronized" है, जो एक मान प्राप्त करती है कि आईसीओलेक्शन तक पहुंच सिंक्रनाइज़ (थ्रेड सुरक्षित) है या नहीं। ConcurrentDictionary इस प्रॉपर्टी से झूठा रिटर्न देता है, और यदि आप इसे पढ़ने का प्रयास करते हैं तो SyncRoot प्रॉपर्टी एक अपवाद फेंकता है। "सिंक्रनाइज़ किए गए कॉन्कुरेंट डिक्शनरी" नाम का अर्थ यह इंगित करने के लिए किया जा सकता है कि संग्रह सिंक्रूट के माध्यम से सिंक्रनाइज़ किया गया है, जो गलत है। –

संबंधित मुद्दे