【发布时间】:2015-07-02 21:35:59
【问题描述】:
我的一些代码中出现了一个棘手的问题。我有一个缓存管理器,它要么从缓存中返回项目,要么调用委托来创建它们(昂贵)。
我发现我的方法的 finalize 部分在与其他线程不同的线程上运行时遇到问题。
这是一个精简版
public IEnumerable<Tuple<string, T>> CacheGetBatchT<T>(IEnumerable<string> ids, BatchFuncT<T> factory_fn) where T : class
{
Dictionary<string, LockPoolItem> missing = new Dictionary<string, LockPoolItem>();
try
{
foreach (string id in ids.Distinct())
{
LockPoolItem lk = AcquireLock(id);
T item;
item = (T)resCache.GetData(id); // try and get from cache
if (item != null)
{
ReleaseLock(lk);
yield return new Tuple<string, T>(id, item);
}
else
missing.Add(id, lk);
}
foreach (Tuple<string, T> i in factory_fn(missing.Keys.ToList()))
{
resCache.Add(i.Item1, i.Item2);
yield return i;
}
yield break; // why is this needed?
}
finally
{
foreach (string s in missing.Keys)
{
ReleaseLock(l);
}
}
}
获取和释放锁用已被 Monitor.Enter / Monitor.Exit 锁定的 LockPoolItem 对象填充字典 [我也尝试过互斥锁]。当在与调用 AcquireLock 的线程不同的线程上调用 ReleaseLock 时,问题就出现了。
从另一个使用线程的函数调用 this 时会出现问题,有时会调用 finalize 块,这是由于在返回的迭代上运行的 IEnumerator 的处置。
下面的块是一个简单的例子。
BlockingCollection<Tuple<Guid, int>> c = new BlockingCollection<Tuple<Guid,int>>();
using (IEnumerator<Tuple<Guid, int>> iter = global.NarrowItemResultRepository.Narrow_GetCount_Batch(userData.NarrowItems, dicId2Nar.Values).GetEnumerator()) {
Task.Factory.StartNew(() => {
while (iter.MoveNext()) {
c.Add(iter.Current);
}
c.CompleteAdding();
});
}
当我添加 yield break 时,这似乎没有发生 - 但是我发现这很难调试,因为它只是偶尔发生。但是,它确实发生了 - 我已经尝试记录线程 ID 并最终确定是否在不同的线程上被调用...
我确定这不是正确的行为:我不明白为什么 dispose 方法(即退出使用)会在不同的线程上被调用。
任何想法如何防止这种情况?
【问题讨论】:
-
我建议您在屈服点持有锁的任何设计都是一个损坏的设计 - 您不知道在您的调用者下一次调用
MoveNext之前需要多长时间或者,事实上,正如您所发现的,Dispose。在不了解您的具体问题的情况下,很难提供具体的建议,但这就是我要寻找的地方 - 更改设计,以便您在释放锁时不受调用者的摆布。 -
这是一个公平的观点,但它并没有回答这个问题。我想要实现的是让提供者在从慢速存储中检索项目时返回项目 - 有些项目可能需要几秒钟,有些可能需要几毫秒,但没有办法事先知道批次中的哪些项目会很慢返回。我怀疑我最好将阻塞集合提供给缓存功能并在那里填充。但是我仍然不明白为什么 dispose/finalize 在不同的线程上被调用。
-
不,它没有,因此它作为评论发布。如果您愿意进行此类更改,我可以付出一些努力并向您展示替代方案的外观。如果您查看我在 supercat 的答案下面的讨论,您会发现我已经争论了好几天,不能保证枚举将在同一个线程上恢复(特别是如果调用代码使用现代功能,例如 @ 987654325@)
-
谢谢 Damien,我有很多关于如何改进这一点的想法,我只是无法弄清楚为什么 Dispose/finalize 在位于 using 块中时在不同的线程上被调用。我现在要装箱并重新启动我的缓存模型。
标签: c# thread-safety dispose yield-return finalize