2

在我开始之前,我应该提一下,我觉得我在这里搞错了。但无论如何我们都走了:

假设我们有以下类:

public class SomeObject {
    public int SomeInt;
    private SomeObject anotherObject;

    public void DoStuff() {
        if (SomeCondition()) anotherObject.SomeInt += 1;
    }
}

现在,假设我们有这些SomeObjects 的集合:

IList<SomeObject> allObjects = new List<SomeObject>(1000);
// ... Pretend the list is populated with 1000 SomeObjects here

假设我打电话DoStuff()给每个人,就像这样:

foreach (var @object in allObjects) @object.DoStuff();

到目前为止一切都很好。现在,让我们假设对象DoStuff()被调用的顺序并不重要。假设这SomeCondition()可能在计算上很昂贵。我可以利用我机器上的所有四个内核(并可能获得性能提升):

Parallel.For(0, 1000, i => allObjects[i].DoStuff());

现在,忽略变量访问原子性的任何问题,当我在循环中时,我不在乎是否有任何给定SomeObject的看到anotherObjector的过时版本SomeInt。* 但是,一旦循环完成,我想确保我的主工作线程(即调用 Parallel.For 的线程)确实看到所有内容都是最新的。

使用 Parallel.For 是否可以保证这一点(例如某种内存屏障?)?还是我需要自己做出某种保证?还是没有办法做出这种保证?

最后,如果我Parallel.For(...)之后以同样的方式再次调用,所有工作线程是否都在使用新的、最新的值?


(*) 的实现者DoStuff()无论如何都对处理顺序做出假设是错误的,对吧?

4

2 回答 2

1
        var locker = new object();
        var total = 0.0;
        Parallel.For(1, 10000000,
        i => { lock (locker) total += (i + 1); });
        Console.WriteLine("WithLocker" + total);

        var total2 = 0.0;
        Parallel.For(1, 10000000,
        i => total2 += (i + 1));
        Console.WriteLine("WithoutLocker" + total2);
        Console.ReadKey();

        // WithLocker 50000004999999
        // WithoutLocker 28861729333278

我为你做了两个例子,一个带储物柜,一个不看结果!

于 2013-09-07T21:21:00.897 回答
1

这里有两个问题。

但是,一旦循环完成,我想确保我的主工作线程(即调用 Parallel.For 的那个)确实看到所有内容都是最新的。

回答你的问题。是的,一旦您Parallel.For完成了所有对的调用DoStuff,您的阵列将不会再看到任何更新。

现在,忽略变量访问原子性的任何问题,当我在循环中时,我不在乎任何给定的 SomeObject 是否看到 anotherObject 或 SomeInt 的过时版本。*

如果你想要一个正确的答案,我真的怀疑你不关心这个。Bassam 的回答解决了代码中潜在的数据竞争问题。如果一个线程正在运行DoSomething,并且这会写入数组中的另一个索引,而该索引同时被另一个线程读取,那么您将看到不确定的结果。锁定可以解决这个问题(如上所示),但会牺牲性能。为每次更新锁定每个线程可以有效地序列化您的工作。我怀疑 Bassam 的锁定示例实际上并没有比非锁定示例运行得更快甚至可能更慢,尽管它确实产生了正确的答案。

IfSomeObject::anotherObject指的是除了this你有潜在的竞争条件之外的任何东西。考虑 whereanotherObject指的是与当前对象相邻的数组中的元素的情况。当这些同时运行时会发生什么?一个线程的代码将尝试读取一个实例,SomeObject而另一个线程写入它。写入不能保证原子发生,你读到我返回一个半写状态的对象。

这在一定程度上取决于 SomeObject 中更新的内容以及更新方式。例如,如果您所做的只是增加一个整数值,您可以使用互锁操作以线程安全的方式增加该值,或者使用临界区或锁来确保您SomeObject实际上是线程安全的。添加同步操作通常会影响性能,因此如果可能的话,我建议寻找一种不需要添加同步的方法。

您可以通过以下两种方式之一解决此问题。

1)如果数组中的每个实例anotherObject保证只通过一次调用更新一次,allObjects[i].DoStuff()那么您可以修改代码以具有输入和输出数组。这可以防止任何竞争条件,因为读取和写入不再冲突。这意味着您需要数组的两个副本,并且它们都需要初始化。

2)如果您要多次更新数组项,或者有两个数组SomeObject不是一个选项,并且SomeCondition()是您的方法中唯一计算成本高的部分,那么您可以将其并行化,然后按顺序更新数组。

IList<bool> allConditions = new List<bool>(1000);
Parallel.For(0, 1000, i => SomeCondition(i)) // Write allConditions not allObjects
for (int i = 0; i < 1000; ++i) { @object.DoStuff(allConditions[i]); }

所以你的观察:

这是有趣的。这意味着 Parallel.For 基本上只对已经是线程安全的代码有用......该死

并不完全正确。您的代码Parallel.For必须是线程安全的,或者不能以非线程安全的方式访问数据和资源。换句话说,如果您可以重新排列代码以保证没有竞争条件(或死锁),则不必锁定,因为没有线程写入相同的数据或读取另一个线程可能正在写入的数据。请注意,并发读取是可以的。

于 2013-09-08T18:19:20.833 回答