什么是常见的并发陷阱?

时间:2009-02-06 15:56:30

标签: multithreading concurrency design-patterns

我正在研究如何让我们的团队了解并发性。开发人员陷入周围并发的最常见陷阱是什么?例如,在.Net中,关键字static为许多并发问题打开了大门。

是否有其他设计模式不是线程安全的?

更新

这里有很多很棒的答案很难选择一个作为公认的答案。请务必滚动浏览所有提示。

21 个答案:

答案 0 :(得分:17)

这个帖子已经有很多好的答案和指示,但是我要补充一点。

不依赖于测试以找出比赛条件和死锁

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

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

您必须调整流程,以更加重视代码审核,由您的最佳思想进行。仅针对并发问题进行单独的代码审查并不是一个坏主意。

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

您必须更加强调代码中的偏执健全性检查,以便尽可能地检测出错误,而不是50,000行代码。

偏执狂。非常偏执。

答案 1 :(得分:15)

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

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

答案 2 :(得分:12)

我和朋友和同事一起教授并发。以下是一些重大缺陷:

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

我也看到了:

  • thread_fork()fork()之间的混淆。
  • 在一个线程中分配内存并在另一个线程中分配free() d时的混淆。
  • 由于某些库是线程安全而某些库不是。
  • 导致的混淆
  • 当他们应该使用睡眠时使用旋转锁的人醒来,或选择,或语言支持的任何阻止机制。

答案 3 :(得分:11)

并发并没有太多陷阱。

然而,同步对共享数据的访问非常棘手。

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

  1. 什么是InterlockedIncrement?
  2. 为什么InterlockedIncrement需要以汇编语言级别存在?
  3. 什么是读写重新排序?
  4. 什么是volatile关键字(在c ++中)以及何时需要使用它?
  5. 什么是同步层次结构?
  6. 什么是ABA问题?
  7. 什么是缓存一致性?
  8. 什么是记忆障碍?
  9. “共享一切”并发是一种极其漏洞的抽象。改为采用shared nothing message passing

答案 4 :(得分:10)

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

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

答案 5 :(得分:3)

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

大多数人在考虑并发性时,会在一个过程中考虑多线程。

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

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

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

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

如果没有一定程度的关注,你不可能使多个锁工作得很好,这在实际系统中是非常罕见的。

祝你好运!

答案 6 :(得分:3)

要记住的一个事实是,即使最初的开发人员让他们的任务模型正常工作(这是一个很大的问题),那么随后的维护团队肯定会以难以想象的方式搞砸了。从中获取的是限制整个系统的并发痕迹。尽力确保您的大多数系统都不知道并发正在发生。对于不熟悉任务模型的人来说,这会让无意中搞砸了。

人们常常把线程/任务变得疯狂。一切都在自己的线程上工作。最终结果是几乎每一段代码都必须密切关注线程问题。它迫使其他简单的代码充满锁定和同步混淆。每次我看到这个,系统最终都会成为一个难以维护的混乱。然而,每次我看到这个,原始开发者仍然坚持认为这是一个很好的设计:(

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

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

我发现创建一个能够处理几乎所有并发问题的核心基础结构效果最佳。如果核心基础结构之外的任何线程必须与另一个软件通信,那么它们必须通过核心基础结构。这样,系统的其余部分可以不知道并发性并且并发问题可以由希望了解并发性的人来处理。

答案 7 :(得分:3)

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

答案 8 :(得分:2)

我看到的第一个陷阱是数据共享过多。

我认为处理并发的更好方法之一是多个进程而不是线程。通过这种方式,线程/进程之间的通信严格限于所选择的管道,消息队列或其他通信方法。

答案 9 :(得分:2)

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

话虽这么说,并发支持在编程语言和不同的库之间差异很大。此外,测试驱动开发在检测和解决并发问题方面并没有太大的帮助(尽管瞬态测试失败表明并发问题)。

答案 10 :(得分:1)

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

答案 11 :(得分:1)

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

  1. 为变量提供不必要的广泛范围。例如,当局部范围有时,人们有时会在实例范围内声明变量。这可以为不需要存在的种族创造潜力。

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

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

    public class CourseService {
        private CourseDao courseDao;
        private List courses;
    
        public List getCourses() {
            this.courses = courseDao.getCourses();
            return this.courses;
        }
    }
    

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

答案 12 :(得分:1)

一些经验法则:

(1)在声明变量

时,请注意上下文
  • 写入类属性 (静态)必须同步
  • 必须同步写入实例属性
  • 尽可能保留所有变量(不要将它们放在成员中) 上下文,除非它有意义)
  • 标记只读不可变的变量

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

(3)避免Double Checked Locking

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

(5)避免busy waiting

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

(7)当您处于同步块时,不允许进行客户端控制。

(8)评论!这真的有助于理解另一个人在声明这个部分同步或变量不可变时的想法。

答案 13 :(得分:1)

双重检查锁定是broken,至少在Java中是这样。理解为什么这是真的,以及如何解决它,使您深入了解并发问题和Java的内存模型。

答案 14 :(得分:1)

从锁定内部调用公共类导致死锁

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);
}

答案 15 :(得分:1)

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

答案 16 :(得分:0)

并发问题难以调试。作为一种预防措施,可以完全禁止访问共享对象,而无需使用互斥锁,以便程序员可以轻松遵循规则。我已经通过在OS提供的互斥锁和信号量等周围制作包装器来完成此操作。

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

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

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

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

答案 17 :(得分:0)

另外,您可以查看进程外类型并发问题 例如:
编写文件的编写程序进程和声明该文件的使用者进程。

答案 18 :(得分:0)

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

答案 19 :(得分:0)

组合性。在任何非平凡的系统中,在不同子系统内进行同步的临时方法使得它们之间的交互通常容易出错并且偶尔也是不可能的。请参阅this video,了解即使是最琐碎的代码也容易出现这些问题。

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

答案 20 :(得分:0)

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