确保线程安全的最佳编程方法和方法论是什么?

8
当我从基础编程语言basic、Pascal、COBOL和C转学Java时,我认为最难的是理解面向对象编程(OOP)的术语和概念。经过8年的Java编程实践后,我得出结论:Java编程以及类似的C#编程中最困难的部分是多线程/并发方面。
编写可靠且可扩展的多线程应用程序确实很难!随着处理器趋于“更宽”而不是更快的趋势,这变得非常关键。
当然,最困难的领域是控制线程之间的交互以及由此产生的错误:死锁、竞争条件、过期数据和延迟等。
因此,我的问题是:您采用什么方法或方法论来生成安全的并发代码,同时减轻死锁、延迟和其他问题的潜在风险?我想到了一种有点非传统但在几个大型应用程序中表现良好的方法,我将在对这个问题的详细回答中分享。
15个回答

7
这不仅适用于Java,也适用于线程编程。我发现只要遵循以下准则,就可以避免大多数并发和延迟问题:
1/ 让每个线程运行自己的生命周期(即决定何时死亡)。它可以从外部提示(例如标志变量),但完全由其自己负责。
2/ 让所有线程按相同顺序分配和释放资源 - 这保证了不会发生死锁。
3/ 尽可能短地锁定资源。
4/ 将数据的责任与数据本身一起传递 - 一旦您通知线程该数据是其要处理的,请将其留给它,直到责任重新归还给您。

6
目前有许多技术正在逐渐进入公众意识(比如最近几年)。 其中一个重要的技术是actors。这是Erlang首先引入到网格计算领域的,但也被新语言Scala(JVM上的actors)所采用。虽然actors并不能解决所有问题,但它们确实使得您能够更轻松地理解代码并确定问题点。由于actors强制您使用共享可变状态下的继续传递,因此它们还使得设计并行算法变得更加简单。
如果您正在使用JVM,则应该查看Fork/Join。Doug Lea撰写了关于该主题的开创性论文,但多位研究人员多年来都对其进行了讨论。据我所知,Doug Lea的参考框架将被纳入Java 7中。
在较低级别上,简化多线程应用程序通常只需要减少锁定的复杂性。Java 5风格的细粒度锁定对于吞吐量非常好,但很难正确使用。Clojure通过软件事务内存(STM)获得了一种替代锁定的方法,并且正在逐渐流行。这本质上与传统锁定相反,因为它是乐观而不是悲观的。您首先假设不会有任何冲突,然后允许框架在发生问题时进行修复。数据库通常以这种方式工作。它非常适合低冲突率系统的吞吐量,但其最大优点在于算法的逻辑组件化。您只需将危险代码包装在事务中,让框架解决其他问题。像GHC的STM monad或我实验性的Scala STM这样的良好STM实现甚至可以获得相当多的编译时检查。
构建并发应用程序有许多新选项,您选择哪个取决于您的专业知识、语言和要建模的问题类型。总的来说,我认为actors与持久的不可变数据结构是一个可靠的选择,但正如我所说,STM则更少侵入性,并且有时可以立即产生更明显的改进。

5
  1. 尽可能避免在线程之间共享数据(复制一切)。
  2. 尽可能不要在方法调用外部对象时使用锁。
  3. 尽可能短时间保持锁定状态。

我会将第一点中的括号注释从(复制所有内容)改为(复制或转移所有内容)。 - Stephen C. Steel

5

在Java中,关于线程安全性并没有一个真正的标准答案。然而,至少有一本非常好的书: Java Concurrency in Practice。我经常参考它(尤其是当我出差时使用在线Safari版本)。

我强烈建议您深入阅读这本书。您可能会发现,您的非传统方法的成本和收益得到了深入的研究。


4
我通常采用Erlang风格的方法,使用Active Object模式。其工作方式如下。
将应用程序分成非常粗粒度的单元。在我的一个当前应用程序(400,000 LOC)中,我有大约8个这样的粗粒度单元。这些单元完全不共享数据。每个单元保持自己的本地数据。每个单元都在自己的线程上运行(= Active Object模式),因此是单线程的。您不需要在单元内部使用任何锁。当单元需要向其他单元发送消息时,它们通过将消息发布到其他单元的队列来实现。另一个单元从队列中选择消息并对该消息做出反应。这可能会触发其他单元的其他消息。 因此,这种类型的应用程序中唯一的锁定是围绕队列的(每个单元一个队列和锁定)。这种架构在定义上是死锁自由的!
这种架构的扩展能力非常好,并且很容易实现和扩展,只要您理解了基本原理。我喜欢把它看作是应用程序内的SOA。
将应用程序分成单元时,请记住。每个CPU核心的最佳长时间运行线程数为1。

将你的应用程序分成单元后,请记住:每个CPU核心的最佳长时间运行线程数量为1。 - Tatvamasi

3
我建议使用基于流的编程,也称为数据流编程。它使用面向对象编程和线程,我认为这是一个自然的前进步骤,就像面向对象编程对过程化编程一样。必须说,数据流编程并不适用于所有情况,它不是通用的。
维基百科上有关于该主题的好文章:

http://en.wikipedia.org/wiki/Dataflow_programming

http://en.wikipedia.org/wiki/Flow-based_programming

此外,它有几个优点,如惊人的灵活配置、分层;程序员(组件程序员)不必编写业务逻辑,这是在另一个阶段完成的(将处理网络组合在一起)。
你知道吗,make 是一个数据流系统?看看 make -j,特别是如果你有多核处理器。

0

Actor模型是您正在使用的,它是迄今为止最简单(和高效)的多线程处理方式。基本上,每个线程都有一个(同步的)队列(可以依赖于操作系统或不依赖),其他线程生成消息并将其放入将处理消息的线程的队列中。

基本示例:

thread1_proc() {

msg = get_queue1_msg(); // block until message is put to queue1
threat1_msg(msg);

}

thread2_proc() {
msg = create_msg_for_thread1();
send_to_queue1(msg);
}

这是一个典型的生产者消费者问题示例。


0

看起来你的IOC有点像FBP :-) 如果JavaFBP代码能够得到像你这样精通编写线程安全代码的人的彻底审查,那将是非常棒的... 它在SourceForge的SVN上。


@Paul:“精通”……只有我的屁股上有多个咬痕才能算得上是精通。但说真的,主要问题在于找到带宽。我肯定会审查代码,但何时、如何仔细地以及如何批判性地审查,我还不确定。 - Lawrence Dol

0

一些专家认为,回答你的问题的方法是完全避免使用线程,因为几乎不可能避免意外问题。引用线程问题中的话:

我们开发了一个过程,其中包括代码成熟度评级系统(有四个级别:红色、黄色、绿色和蓝色)、设计审查、代码审查、夜间构建、回归测试和自动化代码覆盖度指标。确保程序结构的一致视图的内核部分于2000年初编写,经过黄色的设计审查和绿色的代码审查。审阅人员包括并发专家,而不仅仅是没有经验的研究生(Christopher Hylands(现在是Brooks)、Bart Kienhuis、John Reekie和[Ed Lee]都是审阅人员)。我们编写了回归测试,实现了100%的代码覆盖率......该系统本身开始被广泛使用,每次使用该系统都会运行此代码。直到四年后的2004年4月26日,才出现死锁问题。


0

设计多线程新应用程序的最安全方法是遵守以下规则:

不要在设计之下进行设计。

这是什么意思呢?

想象一下,您已经确定了应用程序的主要构建块。让它成为GUI、一些计算引擎。通常,一旦团队规模足够大,团队中的一些人会要求“库”来在这些主要构建块之间“共享代码”。虽然在开始时定义主要构建块的线程和协作规则相对容易,但所有这些努力现在都面临着危险,因为“代码重用库”将被糟糕地设计,需要时进行设计,并且布满了感觉正确的锁和互斥量。这些临时库是您设计之下的设计,也是您线程架构的主要风险。

该怎么办呢?

  • 告诉他们,你宁愿有代码重复,也不要在线程边界共享代码。
  • 如果你认为项目真的会从一些库中受益,建立规则,这些库必须是无状态和可重入的。
  • 你的设计正在发展,一些“公共代码”可以被“提升”到设计中成为应用程序的新主要构建块。
  • 远离网络上的炫酷库。一些第三方库确实可以节省很多时间。但是每添加一个第三方库,你遇到线程问题的风险就增加了。

最后但并非最不重要的是,考虑在你的主要构建块之间进行一些基于消息的交互;例如,参见经常提到的Actor模型。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接