【问题标题】:Could locking an enumerable potentially cause multiple enumeration?锁定一个可枚举对象可能会导致多个枚举吗?
【发布时间】:2026-02-25 03:00:01
【问题描述】:

我认为 ReSharper 在骗我。

我有这个扩展方法(希望)返回两个枚举的异或:

public static IEnumerable<T> Xor<T>(this IEnumerable<T> first, IEnumerable<T> second)
{
    lock (first)
    {
        lock (second)
        {
            var firstAsList = first.ToList();
            var secondAsList = second.ToList();

            return firstAsList.Except(secondAsList).Union(secondAsList.Except(firstAsList));
        }
    }
}

ReSharper 认为我正在对两个参数执行 IEnumerable 的多重枚举,如您所见。如果我移除了锁,那我就很满意了。

ReSharper 是对还是错?我认为这是错误的。


edit:我确实意识到我正在多次枚举列表,但 ReSharper 说我正在多次枚举 原始参数,但我没有'认为不是真的。我将两个参数都枚举到一个列表中,这样我就可以执行实际的集合操作,但正如我所见,我实际上并没有迭代多次传递的参数。

例如,如果传递的参数实际上是查询结果,我相信这种方法不会导致集合操作执行查询风暴。我确实理解 ReSharper 警告多次枚举的含义:如果传递的枚举量很重生成,那么如果它们被枚举多次,那么对它们执行多次枚举会慢得多。

此外,移除锁肯定会让 ReSharper 更快乐:

【问题讨论】:

  • 需要考虑的一点是,将代码块包装在 try/finally 块中(这就是 lock 的语法糖)使得块中的代码less可优化.如果没有 try/finally,可能会有一些可以优化的东西,或者 R# 识别为可优化的,因此不会抱怨。我建议比较两个代码块w.r.t。生成的 IL。我还建议您将问题缩小到 AakashM 详细说明的内容——您 在您的示例中枚举了两次,提供一个有警告但没有枚举两次的示例。

标签: c# resharper ienumerable


【解决方案1】:

您确实多次列举了这两个列表。您没有枚举多次作为参数传递的可枚举项。每次调用Except 时都会枚举这两个列表一次。对Union 的调用不会再枚举任何一个序列,而是枚举对Except 的两次调用的结果。

当然,在这样的上下文中多次迭代List 并不是真正的问题;多次迭代不变的列表不会产生负面影响。

lock 语句与序列枚举没​​有任何关系。锁定 IEnumerable 不会对其进行迭代。当然,像这样锁定两个对象,特别是两个不受本段代码范围限制的对象,是非常危险的。如果其他地方的代码(例如对该方法的另一次调用)最终以相反的顺序对相同的对象进行锁定,则很有可能最终使用此庄园中使用的锁使程序死锁。

【讨论】:

  • 我刚刚更新了我原来的问题;移除锁确实让 ReSharper 认为问题已解决。
  • @user3466413 我的回答仍然回答了你的问题,即使在编辑之后。
  • 如果锁定参数是不正确的方法,你会如何建议使这个线程安全?我的目标是确保枚举不会在另一个线程调用Xor&lt;T&gt; 的过程中被修改。
  • @user3466413 这个方法不应该负责处理这种情况。应该编写调用代码,使其不会在从其他线程访问的序列上调用此方法。
  • @user3466413 “锁定”一个对象不会使其不可变。其他方法仍然可以在锁定时修改锁定对象的内容。
【解决方案2】:

这有点搞笑。

首先要做的事情:正如您正确识别的那样,R# 正在针对 Lists 的多种用法提出此检查不是 - 当然有无需担心多重枚举List - 但反对(R' 认为是多种用法)IEnumerable 参数。我假设您已经知道为什么这可能会很糟糕,所以我将跳过它。


现在到 R# 在这里抱怨是否正确的问题。引用 C# 规范,

形式的锁语句

lock (x) ...

其中x 是引用类型的表达式,精确地 相当于

System.Threading.Monitor.Enter(x);
try {
  ...
}
finally {
  System.Threading.Monitor.Exit(x);
}

除了x 只计算一次。

(我之所以强调这一点,是因为我喜欢这个措辞;它避免了关于这是否是“合成糖”的争论(我绝对没有资格进入)。)

举一个产生此 R# 检查的最小示例:

private static void Method(IEnumerable<int> enumerable)
{
    lock (enumerable)
    {
        var list = enumerable.ToList();
    }
}

并将其替换为我认为规范要求的 完全等效 版本:

private static void Method(IEnumerable<int> enumerable)
{
    var x = enumerable;
    System.Threading.Monitor.Enter(x);
    try
    {
        var list = enumerable.ToList();
    }
    finally
    {
        System.Threading.Monitor.Exit(x);
    }
}

产生检查。

那么问题是:R# 是否正确 进行此检查?这就是我认为我们进入灰色区域的地方。当我将以下枚举传递给这些方法中的任何一个时:

static IEnumerable<int> MyEnumerable()
{
    Console.WriteLine("Enumerable enumerated");
    yield return 1;
    yield return 2;
}

不是多重枚举,这表明R#在这里警告是错误的;但是,我实际上在文档中找不到任何保证lockMonitor.Enter 的这种行为的内容。所以对我来说,它不像this R# bug I reported, where use of GetType flagged this inspection那么明确;不过我猜你是安全的。

如果你提出这个on the R# bug tracker,你可以得到 JetBrains 最好的观察 a) 这种行为是否确实得到保证。 b) 是否可以将 R# 调整为不发出警告或提供警告的理由。


当然,正如其他答案和 cmets 中所述,在这里使用锁定可能实际上并没有实现您想要实现的目标......

【讨论】: