为什么并行此代码有时会工作?

时间:2011-09-02 15:55:23

标签: c# performance synchronization parallel-processing task-parallel-library

我想并行化一段代码,但代码实际上变慢了,可能是因为Barrier和BlockCollection的开销。将有2个线程,其中第一个将找到第二个工作的工作。这两项操作都没有多大工作,因此安全切换的开销很快就会超过两个线程。

所以我想我会尝试自己编写一些代码尽可能精简,而不使用Barrier等。但它并不表现一致。有时它有效,有时却没有,我无法弄清楚为什么。

这段代码只是我用来尝试同步两个线程的机制。它没有做任何有用的事情,只需要重现bug所需的最少代码量。

所以这是代码:

    // node in linkedlist of work elements
        class WorkItem {
            public int Value;
            public WorkItem Next;
        }

        static void Test() {

            WorkItem fst = null; // first element

            Action create = () => {
                WorkItem cur=null;
                for (int i = 0; i < 1000; i++) {                    

                    WorkItem tmp = new WorkItem { Value = i }; // create new comm class

                    if (fst == null) fst = tmp; // if it's the first add it there
                    else cur.Next = tmp;        // else add to back of list

                    cur = tmp; // this is the current one
                }
                cur.Next = new WorkItem { Value = -1 }; // -1 means stop element
#if VERBOSE
                Console.WriteLine("Create is done");
#endif
            };

            Action consume = () => {
                //Thread.Sleep(1); // this also seems to cure it
#if VERBOSE
                Console.WriteLine("Consume starts"); // especially this one seems to matter
#endif

                WorkItem cur = null;
                int tot = 0;

                while (fst == null) { } // busy wait for first one
                cur = fst;
#if VERBOSE
                Console.WriteLine("Consume found first");
#endif
                while (true) {
                    if (cur.Value == -1) break; // if stop element break;
                    tot += cur.Value;

                    while (cur.Next == null) { } // busy wait for next to be set
                    cur = cur.Next; // move to next
                } 
                Console.WriteLine(tot);
            };

            try { Parallel.Invoke(create, consume); }
            catch (AggregateException e) {
                Console.WriteLine(e.Message);
                foreach (var ie in e.InnerExceptions) Console.WriteLine(ie.Message);
            }

            Console.WriteLine("Consume done..");
            Console.ReadKey();
        }

我们的想法是拥有一个工作项链表。一个线程将项添加到该列表的后面,另一个线程读取它们,执行某些操作,并轮询Next字段以查看它是否已设置。一旦设置,它将移动到新的并处理它。它在紧急忙循环中轮询Next字段,因为它应该非常快速地设置。进入睡眠状态,上下文切换等会破坏平行代码的好处。 创建工作项所花费的时间与执行它相当,因此浪费的周期应该非常小。

当我在发布模式下运行代码时,有时它会起作用,有时它什么都不做。问题似乎在'消费者'主题中,'创建'主题似乎总是完成。 (您可以通过摆弄Console.WriteLines来检查)。 它一直在调试模式下工作。在发布它大约50%命中和错过。添加一些Console.Writelines有助于成功率,但即便如此,它也不是100%。 (#define VERBOSE的东西)。

当我在'Consumer'线程中添加Thread.Sleep(1)时,它似乎也解决了它。但是无法重现错误与确定它已经修复不一样。

这里有没有人知道这里出了什么问题?是创建本地副本还是某些无法更新的优化?那样的东西?

没有部分更新权利吗?就像一个datarace,但是那一个线程是写一半,而另一个线程读取部分写入的内存?只是检查..

看着它我认为它应该可以工作..我想每隔几次线程以不同的顺序到达并使其失败,但我不知道如何。我怎么能解决这个问题而不加减速呢?

提前感谢任何提示,

格特 - 扬

2 个答案:

答案 0 :(得分:2)

我尽我所能避免不惜一切代价完全关闭/堆叠互动的雷区。 这可能是一个(语言级)竞争条件,但没有反映Parallel.Invoke我不能确定。基本上,有时fst会被create()更改,有时则不会。理想情况下,它永远不应该被更改(如果c#具有良好的关闭行为)。这可能是由于Parallel.Invoke选择在哪个线程上运行create()和consume()。如果create()在主线程上运行,它可能会在consume()获取它的副本之前更改fst。或者create()可能在一个单独的线程上运行并获取fst的副本。基本上,就像我喜欢c#一样,在这方面它是一个彻头彻尾的痛苦,所以只需解决它并将闭包中涉及的所有变量视为不可变的。

让它运作起来:

//Replace 
WorkItem fst = null
    //with
WorkItem fst = WorkItem.GetSpecialBlankFirstItem();

//And 
if (fst == null) fst = tmp;
   //with
if (fst.Next == null) fst.Next = tmp;

答案 1 :(得分:1)