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

时间:2015-04-24 10:14:30

标签: c# thread-safety dispose yield-return 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块,因为在返回的迭代上运行了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方法(即退出使用)。

任何想法如何防范这个?

3 个答案:

答案 0 :(得分:1)

这里似乎有一场比赛。

看起来你的调用代码创建了枚举器,然后在线程池上启动一个任务来枚举它,然后处理枚举器。我最初的想法是:

  • 如果枚举器在枚举开始之前被释放,则不会发生任何事情。从简短的测试来看,这并不能防止它被处置后的枚举。

  • 如果枚举器在枚举时被释放,则将调用finally块(在调用线程上)并且枚举将停止。

  • 如果任务操作完成了枚举,则将调用finally块(在线程池线程上)。

要尝试演示,请考虑以下方法:

using

如果在枚举之前进行处置,则不会向控制台写入任何内容。这是我怀疑你将在大多数时间做的事情,因为当前线程在任务开始之前到达var enumerator = Items().GetEnumerator(); enumerator.Dispose(); 块的末尾:

Dispose

如果枚举在MoveNext之前完成,则对finally的最终调用将调用var enumerator = Items().GetEnumerator(); enumerator.MoveNext(); enumerator.MoveNext(); enumerator.MoveNext(); 块。

"Before 0"
"Before 1"
"After 1"
"Finally"

结果:

Dispose

如果您在枚举时处置,则对finally的调用将调用var enumerator = Items().GetEnumerator(); enumerator.MoveNext(); enumerator.Dispose(); 块:

"Before 0"
"Finally"

结果:

<virtual-server name="default-host" enable-welcome-root="false">
  <alias name="localhost"/>
  <rewrite name="rule-1" pattern="^/path1(.*)$" substitution="/path2/$1" flags="NC"/>
</virtual-server>

我建议你在同一个帖子上创建,枚举和处理枚举器。

答案 1 :(得分:0)

感谢所有的回复,我意识到发生了什么以及为什么。我问题的解决方案非常简单。我只需要确保在同一个线程上调用所有内容。

        BlockingCollection<Tuple<Guid, int>> c = new BlockingCollection<Tuple<Guid,int>>();

        Task.Factory.StartNew(() => {
            using (IEnumerator<Tuple<Guid, int>> iter = global.NarrowItemResultRepository.Narrow_GetCount_Batch(userData.NarrowItems, dicId2Nar.Values).GetEnumerator()) {

                while (iter.MoveNext()) {
                    c.Add(iter.Current);
                }
                c.CompleteAdding();
            }
        });

答案 2 :(得分:-1)

术语&#34;终结者&#34;涉及一个与最终&#39;完全无关的概念。块&#34 ;;关于终结器的线程上下文没有任何保证,但我认为你真的对&#34;最终&#34;块。

finally包围的yield return块将由迭代器的枚举器上的任何线程调用Dispose执行。调查员通常有权假设在他们身上执行的所有操作,包括Dispose,都将由创建它们的同一个线程完成,并且通常没有义务在任何情况下表现得甚至非常类似于合理的方式他们不是。系统不会阻止代码在多个线程上使用枚举器,但是如果程序使用多个线程,则枚举器不承诺它将在这方面起作用意味着由此产生的任何后果不是枚举器的错误,而是非法使用它的程序的错误。

通常,类可以包含足够的保护以防止无效的多线程,以确保不正确的多线程使用不会导致安全漏洞,但不担心防止任何其他类型的伤害或混淆。