【问题标题】:What are common concurrency pitfalls? [closed]常见的并发陷阱有哪些? [关闭]
【发布时间】:2009-02-06 15:56:29
【问题描述】:

我正在考虑对我们的团队进行并发教育。开发人员陷入围绕并发的最常见陷阱是什么。例如,在 .Net 中,关键字 static 为许多并发问题打开了大门。

还有其他非线程安全的设计模式吗?

更新

这里有很多很棒的答案,很难只选择一个作为接受的答案。一定要滚动浏览它们以获得很棒的提示。

【问题讨论】:

标签: multithreading concurrency design-patterns


【解决方案1】:

这个帖子已经有很多很好的答案和建议了,但让我补充一点。

不要依赖测试来发现比赛条件和死锁

假设您拥有所有良好的开发流程:每个组件的单元测试、每个夜间构建的冒烟测试、要求每个开发人员的更改在签入之前通过测试等等。

这一切都很好,但它导致了一种态度:“好吧,它通过了测试套件,所以它不可能是我的代码中的错误。”这在并发编程中不会很好地为您服务。实时并发错误非常难以重现。您可以在竞争条件下运行一段代码十亿次,然后才会失败。

您将不得不调整您的流程,以更加重视由您最优秀的头脑进行的代码审查。仅针对并发问题进行单独的代码审查并不是一个坏主意。

您将不得不更加重视使您的应用程序能够自我调试。也就是说,当您在测试实验室或客户站点发生故障时,您需要确保捕获并记录了足够的信息,以便您进行明确的事后分析,因为您能够重现错误报告的可能性在您的便利可以忽略不计。

您必须在代码中更加强调偏执的健全性检查,以便在尽可能接近问题的地方检测到错误,而不是在 50,000 行代码之外。

偏执。非常偏执。

【讨论】:

  • 如此真实!在并发方面,人们是最好的调试器,这很奇怪:)
  • +1 竞争条件和死锁很难解决,有时只能出现在“发布”二进制文件中。建立良好的代码审查流程至关重要。
  • 我发现这个答案含糊不清,充满了 FUD。我认为可以更合理地说,如果您遇到竞争条件和死锁问题,那么您的总体线程架构可能存在严重错误,应该重新考虑。如果有的话,从高层架构师来制造竞争条件和死锁不可能。附言性能第二。
【解决方案2】:

一个是race condition,它基本上是假设一段代码将在另一段并发代码之前/之后运行。

还有deadlocks,即代码A等待代码B释放资源Y,而代码B等待A释放资源X。

【讨论】:

    【解决方案3】:

    最大的陷阱之一是首先使用并发。并发会增加大量的设计/调试开销,因此您确实必须检查问题并查看它是否真的需要并发。

    在某些领域并发当然是不可避免的,但是当它可以避免时,就避免它。

    【讨论】:

      【解决方案4】:

      我向我的朋友和同事教授了很多并发知识。以下是一些大陷阱:

      • 假设一个主要在多个线程中读取并且只在一个线程中写入的变量不需要锁定。 (在 Java 中,这种情况可能会导致读取线程永远看不到新值。)
      • 假设线程将按特定顺序运行。
      • 假设线程将同时运行。
      • 假设线程不会同时运行。
      • 假设所有线程都将在任何一个线程结束之前向前推进。

      我也看到了:

      • thread_fork()fork() 之间存在很大的混淆。
      • 在一个线程中分配内存而在另一个线程中分配free()d 时出现混乱。
      • 有些库是线程安全的,有些则不是,这会导致混淆。
      • 人们在应该使用睡眠和唤醒、选择或您的语言支持的任何阻塞机制时使用自旋锁。

      【讨论】:

      • 所以基本上,假设任何关于线程行为的东西都是不好的。 :)
      • 你可以假设每个线程都会在某个时刻向前推进。
      • +1 我记得几个月前读过这个答案,刚才我发现了一个假设关于线程操作的测试,我不得不来找这篇文章只是为了给它投票。跨度>
      • 谢谢!我很高兴能为您服务。
      【解决方案5】:

      并发没有很多陷阱。

      但是,对共享数据的同步访问很棘手。

      以下是编写共享数据同步代码的任何人都应该能够回答的一些问题:

      1. 什么是 InterlockedIncrement?
      2. 为什么 InterlockedIncrement 需要存在于汇编语言级别?
      3. 什么是读写重新排序?
      4. 什么是 volatile 关键字(在 c++ 中)以及何时需要使用它?
      5. 什么是同步层次结构?
      6. 什么是 ABA 问题?
      7. 什么是缓存一致性?
      8. 什么是内存屏障?

      “共享一切”并发是一个非常容易泄漏的抽象。请改用shared nothing message passing

      【讨论】:

      • -1 这份清单仅与 MS-Windows 上的 C++(或 C?)编程有关。
      • 这与许多平台和许多语言相关。 #2 与汇编语言无关。 #7 在大型 SPARC 和 POWER 架构上可能比在大多数 MS-Windows 系统上更相关。事实上,除了 #4 之外,我没有看到任何特定于 C++ 的内容。
      • @vy32 所以你没有看到 InterlockedIncrement 这可能被视为与平台无关的概念,在这种情况下,乍一看这个答案对更多观众来说是有意义的。
      • 好的,所以我在 2010 年写了那条评论,可能还不够政治化。我应该说#2 有多种语言可用,并且与汇编语言没有特别的关系。虽然可以单条指令实现,但也可以用锁等抽象来实现。
      【解决方案6】:

      要记住的一个事实是,即使最初的开发人员让他们的任务模型正常工作(这是一个很大的假设),那么随后的维护团队肯定会以难以想象的方式把事情搞砸。这样做的好处是限制整个系统的并发痕迹。尽你最大的努力确保你的大部分系统都没有意识到并发正在发生。这让不熟悉任务分配模型的人无意中搞砸的机会更少。

      人们经常发疯线程/任务。一切都在自己的线程上工作。最终结果是几乎每一段代码都必须密切关注线程问题。它迫使原本简单的代码充满了锁定和同步混淆。每次我看到这种情况,系统最终都会变得一团糟。然而,每次我看到这个,最初的开发者仍然坚持这是一个很好的设计:(

      就像多重继承一样,如果你想创建一个新的线程/任务,那么在证明不是这样之前,假设你是错的。我什至无法计算我见过的模式线程 A 调用线程 B 然后线程 B 调用线程 C 然后线程 C 调用 D 都在等待来自前一个线程的响应的次数。代码所做的只是通过不同的线程进行冗长的函数调用。当函数调用正常时不要使用线程。

      永远记住,当您想同时工作时,消息队列是您最好的朋友。

      我发现创建一个处理几乎所有并发问题的核心基础架构效果最好。如果核心基础设施之外的任何线程必须与另一块软件通信,那么它们必须通过核心基础设施。这样,系统的其余部分就可以保持对并发的感知,并且可以由希望了解并发的人来处理并发问题。

      【讨论】:

        【解决方案7】:

        正如其他答案所述,两个最可能的问题是死锁竞争条件。但是我的主要建议是,如果您希望就并发问题对团队进行培训,我强烈建议自己进行一些培训。找一本关于这个主题的好书,不要依赖网站上的几段。一本好书取决于您使用的语言:Brian Goetz 的“Java Concurrency in Practice”适合该语言,但还有很多其他的。

        【讨论】:

          【解决方案8】:

          这一切都归结为共享数据/共享状态。如果您不共享数据或状态,那么您就没有并发问题。

          大多数人在想到并发时,会想到单个进程中的多线程。

          考虑这一点的一种方法是,如果您将进程拆分为多个进程会发生什么。他们在哪里必须相互交流?如果您可以清楚进程必须在哪里相互通信,那么您就可以很好地了解它们共享的数据。

          现在,作为心理测试,将这些多个进程转移到单独的机器上。你的沟通模式仍然正确吗?你还能看到如何让它工作吗?如果没有,可能需要重新考虑多个线程。

          (其余部分不适用于 Java 线程,我不使用它,因此对此知之甚少)。

          另一个可能会被抓到的地方是,如果你使用锁来保护共享数据,你应该编写一个锁监视器来为你找到死锁。然后你需要让你的程序处理死锁的可能性。当您遇到死锁错误时,您必须释放所有锁,备份,然后重试。

          如果没有在实际系统中非常罕见的关注水平,您不太可能使多个锁正常工作。

          祝你好运!

          【讨论】:

            【解决方案9】:

            根据我的经验,许多(熟练的)开发人员缺乏并发理论的基础知识。 Tanenbaum 或 Stallings 的经典操作系统教科书很好地解释了并发的理论和含义:互斥、同步、死锁和饥饿。要成功使用并发,必须具备良好的理论背景。

            话虽如此,编程语言和不同库之间的并发支持差异很大。此外,测试驱动开发并不能让您在检测和解决并发问题方面走得太远(尽管短暂的测试失败表明存在并发问题)。

            【讨论】:

              【解决方案10】:

              我看到的 #1 陷阱是过多的数据共享。

              我相信处理并发的更好方法之一是多进程而不是线程。这样,线程/进程之间的通信就被严格限制在所选择的管道、消息队列或其他通信方式上。

              【讨论】:

                【解决方案11】:

                这里有一个关于并发的很好的资源,特别是在 Java 中:http://tech.puredanger.com/ Alex Miller 列出了在处理并发时可能遇到的许多不同问题。强烈推荐:)

                【讨论】:

                  【解决方案12】:

                  从导致死锁的锁中调用公共类

                  public class ThreadedClass
                  {
                      private object syncHandle = new object();
                  
                      public event EventHandler Updated = delegate { };
                      public int state = 0;
                  
                      public void DoSmething()
                      {
                          lock(syncHandle)
                          {
                              // some locked code
                              state = 1;
                  
                              Updated(this, EventArgs.Empty);
                          }
                      }
                  
                      public int State { 
                          get
                          {
                              int returnVal;
                              lock(syncHandle)
                                  returnVal = state;
                              return returnVal;            
                          }
                      }
                  }
                  

                  您无法确定您的客户会调用什么,他们很可能会尝试读取 State 属性。改为这样做

                  public void DoSmething()
                  {
                      lock(syncHandle)
                      {
                          // some locked code
                          state = 1;
                      }
                      // this should be outside the lock
                      Updated(this, EventArgs.Empty);
                  }
                  

                  【讨论】:

                  • 虽然有两个 cmets:1) 锁是可重入的,因此如果从 Updated 事件处理程序中读取 State,它不会导致死锁,并且 2) 解决问题的方法是移除在这种特定情况下可能出现死锁,但会改变语义,即从更新通知操作中删除原子性,这可能会导致潜在的竞争条件。
                  【解决方案13】:

                  双重检查锁定是broken,至少在Java中是这样。了解为什么会出现这种情况以及如何解决它,可以让您深入了解并发问题和 Java 的内存模型。

                  【讨论】:

                  • IIRC Java 内存模型已经改变,它不再“损坏”——只是完全不必要的过早优化。
                  • 您可以通过将变量声明为 volatile 或使用 AtomicReference 来解除它 - 但这会在每次访问时强制设置内存屏障。裸双重检查锁肯定还是坏了。
                  【解决方案14】:

                  一些经验法则:

                  (1) 声明变量时注意上下文

                  • 写入类属性 (static) 必须同步
                  • 必须同步写入实例属性
                  • 尽可能将所有变量保持在本地(不要将它们放在成员中 除非有意义)
                  • 标记只读不可变变量

                  (2) 锁定对可变类或实例属性的访问:属于同一个invariant 的变量应该受到同一个锁的保护。

                  (3) 避免Double Checked Locking

                  (4) 在运行分布式操作(调用子程序)时保持锁。

                  (5) 避免busy waiting

                  (6) 保持同步部分的低工作量

                  (7) 不允许在同步块中进行客户端控制。

                  (8) 条评论!这确实有助于理解其他人在声明此部分同步或该变量不可变时的想法。

                  【讨论】:

                    【解决方案15】:

                    并发编程的一个缺陷是不正确的封装会导致竞争和死锁。这可能会以多种不同的方式发生,尽管我特别看到了两种:

                    1. 给变量提供不必要的广泛范围。例如,有时人们在实例范围内声明变量,而本地范围可以这样做。这可以为不需要的比赛创造潜力。

                    2. 不必要地暴露锁。如果不需要暴露锁,则考虑将其隐藏起来。否则客户端可能会使用它并创建您本可以阻止的死锁。

                    这是一个简单的上面 #1 的示例,它与我在生产代码中看到的非常接近:

                    公共类 CourseService { 私人 CourseDao courseDao; 私人名单课程; 公共列表 getCourses() { this.courses = courseDao.getCourses(); 返回 this.courses; } }

                    在此示例中,courses 变量不需要具有实例范围,现在对 getCourses() 的并发调用可以有竞争。

                    【讨论】:

                    • 私人列表课程必须是静态的才会成为问题吗?
                    • 不。如果一个 CourseService 实例被多个线程共享,那么就有可能发生竞争。
                    【解决方案16】:

                    刚找到这篇论文,听起来很有趣:A Study of Common Pitfalls in Simple Multi-Threaded Programs

                    【讨论】:

                      【解决方案17】:

                      一些典型的陷阱是deadlocks(两个竞争进程卡住等待对方释放一些资源)和race conditions(当事件的时间和/或依赖性可能导致意外行为时)。这也是一个值得的video about "Multithreading Gotchas"

                      【讨论】:

                      • 废话,你比我快 12 秒。 :)
                      • 我不会对“竞争条件”开明显的玩笑。
                      【解决方案18】:

                      您还可以查看进程外类型并发问题
                      例如:
                      一个编写文件的写入进程和一个声明该文件的消费者进程。

                      【讨论】:

                        【解决方案19】:

                        并发问题非常难以调试。作为一种预防措施,可以在不使用互斥锁的情况下完全禁止访问共享对象,这样程序员就可以轻松地遵循规则。我已经看到通过围绕操作系统提供的互斥锁和信号量等进行包装来完成此操作。

                        以下是我过去的一些令人困惑的例子:

                        我曾经为 Windows 开发打印机驱动程序。为了防止多个线程同时写入打印机,我们的端口监视器使用了这样的结构: // 伪代码,因为我不记得 API BOOL OpenPort() { GrabCriticalSection(); } BOOL ClosePort() { ReleaseCriticalSection(); } BOOL WritePort() { writestuff(); }

                        不幸的是,对 WritePort 的每次调用都来自假脱机程序线程池中的不同线程。我们最终遇到了 OpenPort 和 ClosePort 被不同的线程调用,导致死锁的情况。这个问题的解决方案留作练习,因为我不记得我做了什么。

                        我以前也从事打印机固件方面的工作。在这种情况下,打印机使用称为 uCOS(发音为“粘液”)的 RTOS,因此每个功能都有自己的任务(打印头电机、串行端口、并行端口、网络堆栈等)。该打印机的一个版本有一个内部选项,可插入打印机主板上的串行端口。在某个时候,发现打印机会从该外围设备读取相同的结果两次,并且之后的每个值都会乱序。 (例如,外围设备读取序列 1,7,3,56,9,230 但我们会看到 1,7,3,3,56,9,230。这个值被报告给计算机并输入数据库,所以有一堆ID 号错误的文档数量非常糟糕)其根本原因是未能遵守保护设备读取缓冲区的互斥锁。 (因此我在此回复开头的建议)

                        【讨论】:

                          【解决方案20】:

                          这不是一个陷阱,而更多的是基于其他人的反应的提示。 .NET 框架的 readerwriterlockslim 将在许多情况下显着提高您的性能,而不是“锁定”语句,同时是可重入的。

                          【讨论】:

                            【解决方案21】:

                            可组合性。在任何重要的系统中,不同子系统内同步的特殊方法使它们之间的交互通常容易出错,有时甚至是不可能的。请参阅this video 了解即使是最琐碎的代码也容易出现这些问题的示例。

                            就个人而言,我是Actor model of concurrent computation(异步变体)的转换者。

                            【讨论】:

                            猜你喜欢
                            • 1970-01-01
                            • 2012-06-30
                            • 1970-01-01
                            • 2010-10-04
                            • 1970-01-01
                            • 2011-12-11
                            • 2010-09-27
                            • 1970-01-01
                            • 2012-06-05
                            相关资源
                            最近更新 更多