并行操作的线程安全

时间:2013-09-07 20:19:11

标签: c# multithreading parallel-processing thread-safety

在我开始之前,我应该提一下,我觉得我在这里得到了错误的结局。但无论如何我们走了:

想象一下,我们有以下课程:

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

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

现在,假设我们收集了这些SomeObject s:

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看到过时版本的anotherObject或{{1}然而,一旦循环完成,我想确保我的主工作线程(即调用Parallel.For的那个)能够看到所有内容都是最新的。

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

最后,如果我刚刚以同样的方式再次调用SomeInt,那么所有工作线程是否都会使用新的最新值来处理所有内容?


(*)Parallel.For(...)的实施者无论如何都要对处理顺序做出假设,对吗?

2 个答案:

答案 0 :(得分: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

我为你制作了两个带锁柜的例子,一个没看结果!

答案 1 :(得分:1)

这里有两个问题。

  

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

回答你的问题。是的,在Parallel.For完成后,DoStuff的所有来电都将完成,您的阵列将不会再看到更新。

  

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

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

如果SomeObject::anotherObject指的是this以外的任何内容,则表示您有潜在的竞争条件。考虑anotherObject引用与当前对象相邻的数组中的元素的情况。当这些并发运行时会发生什么?一个线程的代码将尝试读取SomeObject的实例,而另一个线程写入它。写保证以原子方式发生,您读取我的返回对象处于半写状态。

这取决于SomeObject中正在更新的内容以及它是如何更新的。例如,如果你所做的只是递增一个整数值,你可以使用Interlocked Operations以线程安全的方式递增值,或者使用关键部分或锁来确保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中的代码必须是线程安全的,或者不以非线程安全的方式访问数据和资源。换句话说,如果您可以重新排列代码以保证没有竞争条件(或死锁),则不必锁定,因为没有线程写入相同的数据或将读取另一个线程可能正在写入的数据。请注意,并发读取正常。