我的一些代码中出现了一个棘手的问题。我有一个缓存管理器,它要么从缓存中返回项目,要么调用委托(delegate)来创建它们(代价高昂)。
我发现我的方法的 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 对象填充字典 [我也尝试过互斥锁]。当调用 ReleaseLock 的线程与调用 AcquireLock 的线程不同时,问题就来了。
当从另一个使用线程的函数调用它时,有时会调用 finalize block ,这是由于在返回的迭代上运行的 IEnumerator 的处置。
下面的 block 是一个简单的例子。
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 方法(即退出使用)。
有什么预防方法吗?
最佳答案
这里好像有一场比赛。
看起来你的调用代码创建了枚举器,然后在线程池上启动一个任务来枚举它,然后处理枚举器。我的初步想法:
如果枚举器在枚举开始之前被释放,则什么也不会发生。从简短的测试来看,这不会阻止在处理后进行枚举。
如果枚举器在枚举时被释放,finally block 将被调用(在调用线程上)并且枚举将停止。
如果任务操作完成枚举,将调用 finally block (在线程池线程上)。
要尝试演示,请考虑以下方法:
private static IEnumerable<int> Items()
{
try
{
Console.WriteLine("Before 0");
yield return 0;
Console.WriteLine("Before 1");
yield return 1;
Console.WriteLine("After 1");
}
finally
{
Console.WriteLine("Finally");
}
}
如果您在枚举之前进行处置,则不会向控制台写入任何内容。这是我怀疑你大部分时间会做的事情,因为当前线程在任务开始之前到达 using
block 的末尾:
var enumerator = Items().GetEnumerator();
enumerator.Dispose();
如果枚举在 Dispose
之前完成,则对 MoveNext
的最终调用将调用 finally
block 。
var enumerator = Items().GetEnumerator();
enumerator.MoveNext();
enumerator.MoveNext();
enumerator.MoveNext();
结果:
"Before 0"
"Before 1"
"After 1"
"Finally"
如果您在枚举时处置,对 Dispose
的调用将调用 finally
block :
var enumerator = Items().GetEnumerator();
enumerator.MoveNext();
enumerator.Dispose();
结果:
"Before 0"
"Finally"
我建议您在同一个线程上创建、枚举和处理枚举器。
关于c# - 在 yield return 函数中是否可以确保在同一个线程上调用终结器?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29844814/