当前位置: 首页 > 编程语言 > C#

线程安全的memoization分享

时间:2023-04-11 01:40:29 C#

线程安全的memoization先从WesDyer的函数memoization方法说起:publicstaticFuncMemoize(thisFuncf){varmap=newDictionary();返回=>{R值;if(map.TryGetValue(a,outvalue))返回值;值=f(a);map.Add(a,value);返回值;};问题是,当从多个线程使用时,我们可能会遇到麻烦:Funcf=...varf1=f.Memoize();...在线程1中:vary1=f1(1);在线程2中:vary2=f1(1);//我们可能会在这里重新计算f(1)!我们尽量避免这种情况。锁定映射:publicstaticFuncMemoize(thisFuncf){varmap=newDictionary();返回=>{R值;lock(map){if(map.TryGetValue(a,outvalue))返回值;值=f(a);map.Add(a,value);}返回值;};}显然是一个糟糕的想法,因为它阻止我们同时计算许多不同参数的f1。如果a具有值类型,则锁定a将不起作用(无论如何这都是一个坏主意,因为我们无法控制并且外部代码也可以锁定它)。这里有两个我能想到的选项:假设一个计算Lazy的Lazy类(见这里):returna=>{惰性结果;lock(map){if(!map.TryGetValue(a,outresult)){result=()=>f(a);地图。添加(一个,结果);}}返回结果。价值;};}或者保留一个额外的对象字典用于同步:publicstaticFuncMemoize(thisFuncf){varmap=newDictionary();varmapSync=newDictionary();返回=>{R值;对象同步;锁定(mapSync){如果(!mapSync.TryGetValue(a,不同步)){sync=newobject();mapSync[a]=同步;}}lock(map){if(map.TryGetValue(a,outvalue))返回值;}锁(同步){值=f(a);锁(地图){地图[a]=值;}返回值;}};有更好的选择吗?ConcurrentDictionary使用.net4.0,没有不必要的懒惰。关键是GetOrAdd(A,Func),它提供了一个非常微不足道的lambda。publicstaticFuncMemoize(thisFuncf){varcache=newConcurrentDictionary();返回a=>cache.GetOrAdd(a,f);};更新上述解决方案确实允许多个同时读取器和写入器以最小的开销。但是,它不会阻止f(a)为同一值多次执行(在计算期间)。如果这对您很重要,您可以将值包装在Lazy中,但每次读取都会产生成本。publicstaticFuncMemoize(thisFuncf){varcache=newConcurrentDictionary>();返回a=>cache.GetOrAdd(a,newLazy(()=>f(a))).Value;对于预填充的1000项缓存的一百万次读取的更新计时测试显示ConcurrentDictionary为19ms-与常规Dictionary相同-但Lazy版本为720ms。如果这听起来太陡峭,您可以使用更复杂的解决方案来两全其美。publicstaticFuncMemoize(thisFuncf){varcache=newConcurrentDictionary();varsyncMap=newConcurrentDictionary();返回=>{Rr;if(!cache.TryGetValue(a,outr)){varsync=syncMap.GetOrAdd(a,newobject());锁(同步){r=cache.GetOrAdd(a,f);}syncMap.TryRemove(a,outsync);}返回r;};如果你已经有了Lazy类型,我假设你使用的是.net4.0,那么你也可以使用ConcurrentDictionary:publicstaticFuncMemoize(thisFuncf){varmap=newConcurrentDictionary>();returna=>{Lazylazy=newLazy(()=>f(a),LazyExecutionMode.EnsureSingleThreadSafeExecution);if(!map.TryAdd(a,lazy)){返回map[a].Value;}返回惰性值;};由于LazyconstructorForenumparameters,Thomas的答案似乎无法在.NET4.0下编译。我在下面修改了它。我还添加了一个可选参数,用于提供您自己的相等比较器。如果TInput没有实现自己的Equals,或者如果TInput是一个字符串并且您希望它不区分大小写,这将很有用。publicstaticFuncMemoize(thisFuncfunc,IEqualityComparercomparer=null){varmap=comparer==null?newConcurrentDictionary():newConcurrentDictionary>(比较器);returninput=>{varlazy=newLazy(()=>func(input),LazyThreadSafetyMode.ExecutionAndPublication);返回map.TryAdd(input,lazy)?惰性值:映射[输入].值;};我用这个作为我的测试对这个方法做了一些基本测试:publicvoidTestMemoize(){FuncmainFunc=i=>{Console.WriteLine("Evaluating"+i);线程.睡眠(1000);返回i.ToString();};varmemoized=mainFunc.Memoize();Parallel.ForEach(Enumerable.Range(0,10),i=>Parallel.ForEach(Enumerable.Range(0,10),j=>Console.WriteLine(memoized(i))));它似乎工作正常。扩展NigelTouch的优秀答案,我想提供一个可重用的组件,从他的解决方案中提取,限制对f(a)的调用次数。我称它为SynchronizedConcurrentDictionary,它看起来像这样:publicnewTValueGetOrAdd(TKeykey,FuncvalueFactory){TValue结果;_cacheLock.EnterWriteLock();尝试{结果=base.GetOrAdd(key,valueFactory);}最后{_cacheLock.ExitWriteLock();}返回结果;}}然后Memoize函数变成两行:publicstaticFuncMemoize(thisFuncf){varcache=newSynchronizedConcurrentDictionary();返回键=>cache.GetOrAdd(key,f);干杯!不,它们不是更好的选择。带有惰性评估的版本毫无意义,因为无论如何您都会立即评估它。带有同步字典的版本不起作用,因为您在使用之前没有在锁中保护地图字典。你称之为可怕的版本实际上是最好的选择。您必须在锁中保护地图字典,以便一次只有一个线程可以访问它。字典不是线程安全的,所以如果你有一个线程从它读取而另一个线程正在更改它,你就会遇到问题。请记住,在地图对象上使用锁并不能保护地图对象本身,它只是使用地图引用作为标识符来同时保留多个线程来运行锁内的代码。您必须将访问对象的所有代码都放在锁中,而不仅仅是更改对象的代码。您不想两次计算相同的值,并且希望许多线程能够同时计算值和/或检索值。为此,您需要使用某种条件变量和细粒度锁定系统。继承人们的思想。当不存在任何值时,您将一个值放入同步映射中,然后任何需要该值的线程都会等待它,否则您将只获得当前值。这样,映射的锁定被最小化到查询值和返回值。publicstaticFuncMemoize(thisFuncf){varmap=newDictionary();varmapSync=newDictionary();返回=>{R值;对象同步=空;布尔计算=假;布尔等待=假;lock(map){if(!map.TryGetValue(a,outvalue)){//它不在地图中if(!mapSync.TryGetValue(a,outsync)){//当前未创建sync=newobject();mapSync[a]=同步;计算=真;}else{calc=false;等待=真;}}}if(calc){锁(同步){value=f(a);锁定(地图){地图.Add(a,值);mapSync.Remove(a);}Monitor.PulseAll(同步);返回值;}}elseif(wait){lock(sync){while(!map.TryGetValue(a,outvalue)){Monitor.Wait(sync);}返回值;}}锁(地图){返回地图[a];}};这只是第一次快速尝试,但我认为它演示了该技术。在这里,您正在用额外的内存换取速度。您是否阅读了Dyer在文章中关于线程安全的评论?使Memoize线程安全的最简单方法可能是锁定地图。这将确保被记忆的函数只对每个不同的参数集运行一次。在我的RoboRally游戏示例中,我实际上使用函数内存来充当“代理单例”。它实际上不是单例,因为每个工厂实例可以有一个实例(除非工厂是静态的)。但这正是我想要的。以上就是C#学习教程:Thread-SafeMemoization分享的全部内容。如果对你有用,需要进一步了解C#学习教程,希望大家多多关注。本文收集自网络,不代表立场。如涉及侵权,请点击右侧联系管理员删除。如需转载请注明出处: