c# - 在 yield return 函数中是否可以确保在同一个线程上调用终结器?

标签 c# thread-safety dispose yield-return finalize

我的一些代码中出现了一个棘手的问题。我有一个缓存管理器,它要么从缓存中返回项目,要么调用委托(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/

相关文章:

java - 调用线程在哪一次获得了对象的内在锁?

c# - GarbageCollector、Dispose 还是静态方法?

c# - 仅针对托管资源的最小 IDisposable implimenation

.net - 包含 using 语句的单元测试方法

c# - 如何在 C# 中用 Dictionary<string,List<String>> 的内容填充 ListView

c# - 静态内存管理

c# - 插入数据库的日期变为 0000

ruby - `Thread.current.object_id` 可以在线程本身内部更改吗?

c# - 基于另一个用户控件中的选择来重新创建用户控件中的动态控件

python - Django 中的线程安全存储