ConcurrentDictionary.GetOrAdd在valueFactory具有副作用时的行为
ConcurrentDictionary.GetOrAdd在valueFactory具有副作用时的行为
我试图通过在数据库服务器中引入缓存层来卸载一些非常核心的函数的工作,这些函数将一个值插入到数据库中的表中并检索id。这是在多线程环境下进行的。\n我的第一种方法是:\n
public class Cache { private Dictionaryi; public void Init() { /* 用数据库中的值初始化i */ } public Int64 Get(string value) lock(i) { Int64 id; if (cache.i.TryGetValue(value, out id)) return id; id = /* 插入到数据库并检索ID */ cache.i[value] = id; return id; } }
\n这有所帮助。然而,线程仍然相互等待很长时间。我想减少这种等待时间。我的第一个想法是使用`ConcurrentDictionary.GetOrAdd(key, valueFactory)`。但是这不起作用,因为valueFactory可能会被调用多次。\n我最终采用了这种方法:\n
public class Cache { private ConcurrentDictionaryi; public void Init() { /* 用数据库中的值初始化i */ } public Int64 Get(string value) { Int64 id; if (i.TryGetValue(value, out id)) return id; lock (i) { if (i.TryGetValue(value, out id)) return id; id = /* 插入到数据库并检索ID */ i.TryAdd(value, id); return id; } } }
\n有没有更好的方法来做这个?这个方法是否线程安全?
ConcurrentDictionary.GetOrAdd when valueFactory has side-effects 这个问题的出现的原因是在使用ConcurrentDictionary.GetOrAdd方法时,如果valueFactory(值工厂)具有副作用,会导致问题的发生。解决这个问题的方法是使用lambda表达式来创建一个“double” lambda,以避免多次创建Lazy实例。
在上述内容中,提到了使用Lazy类的示例,每次调用GetOrAdd方法时都会创建一个Lazy实例。尽管Lazy类的魔力仍然存在,只会调用一次创建实例的Func方法,但以上示例中创建的多个Lazy实例可能解释了在尝试时看到的内存增加现象。
如果创建一个“double” lambda,就不会有多个Lazy实例的实例化。可以将以下代码粘贴到控制台应用程序中,比较带有和不带有下面的x => new Lazy...的实现:
public static class LazyEvaluationTesting { private static readonly ConcurrentDictionary> cacheableItemCache = new ConcurrentDictionary >(); private static CacheableItem RetrieveCacheableItem(int itemId) { Console.WriteLine("--RETRIEVE called\t ItemId [{0}] ThreadId [{1}]", itemId, Thread.CurrentThread.ManagedThreadId); return new CacheableItem { ItemId = itemId }; } private static void GetCacheableItem(int itemId) { Console.WriteLine("GET called\t ItemId [{0}] ThreadId [{1}]", itemId, Thread.CurrentThread.ManagedThreadId); CacheableItem cacheableItem = cacheableItemCache .GetOrAdd(itemId, x => new CustomLazy ( () => RetrieveCacheableItem(itemId) ) ).Value; } public static void TestLazyEvaluation() { int[] itemIds = { 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5 }; ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = 75 }; Parallel.ForEach(itemIds, options, itemId => { GetCacheableItem(itemId); GetCacheableItem(itemId); GetCacheableItem(itemId); GetCacheableItem(itemId); GetCacheableItem(itemId); }); } private class CustomLazy : Lazy where T : class { public CustomLazy(Func valueFactory) : base(valueFactory) { Console.WriteLine("-Lazy Constructor called ThreadId [{0}]", Thread.CurrentThread.ManagedThreadId); } } private class CacheableItem { public int ItemId { get; set; } } }
解决方案中使用了一个CustomLazy类,该类继承自Lazy类,并且只在构造函数中输出一条消息,以便查看是否调用了构造函数。它在其他方面的行为上与Lazy类完全相同。如果运行以上示例代码,只会看到一次输出消息。
需要注意的是,使用回调函数的GetOrAdd方法虽然没有像new Lazy()那样显式分配内存,但它会传递一个来自Func<>类型的对象,而这个对象必须来自某个地方。这个过程的具体实现有很大的灵活性,但需要注意的是,第二个回调函数引用了itemId,因此在大多数实现中需要构建一个闭包。当然,这涉及许多因素,但不能说你的重写总是产生更少的内存分配。
在一个稍微变种的代码中,我使用了.NET Fiddle来进行演示,并输出了更多的信息,以更清楚地说明问题。带有lambda的版本不会多次调用lazy的构造函数。
以上是关于ConcurrentDictionary.GetOrAdd when valueFactory has side-effects这个问题的原因和解决方法的整理。
ConcurrentDictionary.GetOrAdd是一个用于在并发环境下获取或添加值的方法。在使用valueFactory参数时,可能会出现副作用的情况。下面的对话展示了该问题的出现原因以及解决方法。
在这个对话中,讨论了一个需要只创建一次并在创建后可以被任意数量的线程访问的对象的延迟创建问题。最初的解决方法是使用了ConcurrentDictionary和Lazy类的组合来实现延迟创建。
然而,有人提出了一个问题,即在使用Lazy类时是否需要指定LazyThreadSafetyMode.ExecutionAndPublication参数。对此,另一个人解释说,这是Lazy类的默认行为,即除非你明确告诉它不需要完全同步,否则它会认为需要完全同步。
接下来,讨论了这种方法与之前的方法相比,是否更加线程安全和更快。对此,另一个人回答说,两种方法都是线程安全的,但使用Lazy类的方法更快,因为不同线程在初始化值时不需要互相等待。此外,使用Lazy类的方法更清晰和易于使用,因为它使用了更高级别的工具来表示你想要做的事情,因此不会出现错误或误解的可能性。
然而,另一个人提到,他对原始方法进行了修改,删除了存储Lazy对象的部分,以减少内存占用。对此,另一个人指出,这个修改是无效的,因为它会导致在多个线程同时请求同一个值时,该值被计算多次。
在继续讨论中,另一个人提到,在某些部署环境中内存是一个问题,而这个修改导致内存占用增加了35%,超出了内存限制并引发了分页问题。他询问是否有其他的解决方法。
对此,另一个人给出了一个折中的建议,即在内存和性能之间进行权衡。如果需要使用更少的内存,就需要牺牲程序的运行速度。在这种情况下,可能需要使用更细粒度的锁定,这意味着在不需要严格等待的情况下会有更多的等待。如果可以接受降低的运行性能,就不需要那么多的内存。
最后,提问者表示自己的解决方案在性能方面没有问题,但在内存方面存在问题。他想知道是否有更好(更清晰)的实现方法。对此,另一个人回答说,使用Lazy类的方法更加清晰,但由于内存占用的增加并没有带来额外的性能优势,这个修改是不可行的。
这个对话讨论了使用ConcurrentDictionary.GetOrAdd和Lazy类进行延迟创建的问题,并提出了使用更细粒度的锁定来减少内存占用的建议。