【问题标题】:Parallel.Foreach and for each producing different results: Why is my code unsafe?Parallel.Foreach 和 for each 产生不同的结果:为什么我的代码不安全?
【发布时间】:2021-09-13 06:31:20
【问题描述】:

我有一个文本文件,我将其读入字符串content。为了识别我想要进一步处理的文本体,我获取了字符串中关键字的索引,然后将“起始”索引设置为找到的最小索引。

我用Parallel.ForEach 试过这个...

ConcurrentBag<int> indexes = new();
int index;

switch (Case)
{
    case 1:
        Parallel.ForEach(KeywordTypes.GetImplementedNamedObjects(), inos =>
        {
            index = content.IndexOf($"/begin {inos}");
            index = index == -1 ? content.Length : index;
            indexes.Add(index);
        });
        index = indexes.Min();
        return index;

...和foreach:

foreach (string inos in KeywordTypes.GetImplementedNamedObjects())
{
    index = content.IndexOf($"/begin {inos}");
    index = index == -1 ? content.Length : index;
    indexes.Add(index);
}

index = indexes.Min();
return index;

foreach 会产生预期的结果,但 Parallel.ForEach 不会。

为什么我的代码不是线程安全的?

【问题讨论】:

  • @FranzGleichmann Marc 已经发现了问题。我尝试锁定索引,虽然它是一个线程安全的变量,但完全忘记了 int 索引本身,我在 Parallel.Foreach 中声明它会导致对变量的不安全访问。我这样做是因为我在多个 switch 语句中使用它,这就是它失败的原因。
  • 附带说明,如果按插入顺序获取indexes 很重要,您可以从ConcurrentBag&lt;int&gt; 切换到ConcurrentQueue&lt;int&gt;。但一般来说,当您想要进行并行工作并且想要返回结果时,PLINQ 库(AsParallel LINQ 运算符)比Parallel 类更合适。你可以看一个例子here

标签: c# concurrency thread-safety parallel.foreach


【解决方案1】:

这里只有一个index 变量,因为它被“捕获”了。这意味着多个线程可以为它争吵,而不是每个线程都有自己的版本。

考虑:

  • 线程 A 计算 index = content.IndexOf($"/begin {inos}");
  • 线程 B 计算 index = content.IndexOf($"/begin {inos}"); - 糟糕,线程 A 的版本刚刚被覆盖
  • 线程 A 使用 B 刚刚更新的 index 计算 index = index == -1 ? content.Length : index;

关键是:由于线程争用而丢失了一个值。

只需移动index 的声明即可解决此问题:

Parallel.ForEach(KeywordTypes.GetImplementedNamedObjects(), inos =>
{
    var index = content.IndexOf($"/begin {inos}");
    ...

从根本上说,变量的范围是由它的声明位置定义的。如果在本地方法/lambda 外部声明了一个变量,编译器会尊重您的要求,并且该变量在该本地方法/lambda 的所有使用之间共享;如果在本地方法/lambda 内部声明它,则生命周期对于该调用是本地的,调用者之间不共享任何状态。

如果您想绝对确定您没有意外泄漏状态,则 lambda 上的 static 修饰符可以实现这一点,尽管它也可以防止 indexes 被访问,所以......可能不是您需要的。

【讨论】:

  • 伙计,我现在觉得自己很愚蠢。非常感谢!最初我对其进行了正确编码,但开始在多个 switch 案例中使用 index 并将声明移到 Parallel.Foreach 范围之外。但是感谢您再次深入解释!
  • @MichaelS 线程、并行和状态争用几乎是编程中最难的事情;这里没有“愚蠢”——只有新东西要学
猜你喜欢
  • 1970-01-01
  • 2022-01-24
  • 2023-03-14
  • 2016-11-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-08-12
  • 2021-09-09
相关资源
最近更新 更多