【问题标题】:Why does parallel this code work sometimes?为什么并行此代码有时会起作用?
【发布时间】:2011-09-03 00:42:24
【问题描述】:

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

所以我想我会尝试自己编写一些代码以尽可能精简,而不使用 Barrier 等。但是它的行为并不一致。有时有效,有时无效,我不知道为什么。

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

代码如下:

    // 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 的东西)。

当我在“消费者”线程中添加 Thread.Sleep(1) 时,它似乎也可以修复它。但无法重现错误与确定已修复是两回事。

这里有没有人知道这里出了什么问题?是创建本地副本的一些优化还是没有更新的东西?类似的东西?

不存在部分更新之类的东西,对吧?就像一个数据竞赛,但是一个线程写了一半,另一个线程读取了部分写的内存?只是检查..

看着它,我认为它应该可以工作。我猜每隔几次线程以不同的顺序到达,这会导致它失败,但我不明白如何。以及如何在不减慢速度的情况下解决此问题?

提前感谢您的任何提示,

格特-简

【问题讨论】:

  • 如果您的并行代码有时可以工作,那么您就有了所谓的竞争条件。
  • 我知道竞态条件,这就是关于“部分更新”的评论。很确定这不是一个。
  • 你说//Thread.Sleep(1); // this also seems to cure it的事实本身就证明它一个竞争条件。
  • 你说得对,我对数据竞赛感到困惑。

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


【解决方案1】:

我尽我最大的努力,不惜一切代价避免封闭/堆栈交互的彻底雷区。 这可能是一个(语言级别的)竞争条件,但没有反映 Parallel.Invoke 我不能确定。基本上,有时 fst 会被 create() 更改,有时不会。理想情况下,它不应该被改变(如果 c# 有良好的关闭行为)。这可能是由于 Parallel.Invoke 选择在哪个线程上运行 create() 和 consume()。如果 create() 在主线程上运行,它可能会在 consume() 获取 fst 副本之前更改 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;

【讨论】:

  • 为什么要复制fst?闭包语义不是为闭包​​创建一个新的匿名类,并且 fst 被提升为匿名类的成员吗?
  • 是的,共享状态有点脏,我正在努力争取性能.. 调用我的代码这个错误感觉有点刺耳:'-(我会重新编写代码,我不;t不过现在有时间。我会尽快回来。谢谢!
  • 感谢您的规范,吉米。我刚刚稍微更改了帖子,但基本上因为我不完全了解发生了什么,我将其重写为“绕过问题”。我可能不知道枪如何工作的确切细节,但我知道的足够多,可以告诉你不要朝自己的脚开枪。
  • 我还要补充一点,您为该更改获得的性能量将非常小。
  • 问题是fst==null改成fst.Next == null也解决不了问题,值还是可以缓存的。
【解决方案2】:

【讨论】:

  • 您好,谢谢!浏览了这些线程,似乎深入了解它!
猜你喜欢
  • 2011-10-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-24
  • 2021-06-09
  • 1970-01-01
  • 2013-01-10
相关资源
最近更新 更多