线程最佳实践

我所从事的许多项目的线程实现效果不佳,而且我也不得不跟踪这些项目。 有没有一种公认的最好的方式来处理线程。 我的代码总是在等待一个永远不会触发的事件。

我有点像一个devise模式或什么东西。

(假设.NET;类似的东西将适用于其他平台。)

那么,有很多事情要考虑。 我build议:

  • 不变性对于multithreading来说是很好的。 函数式编程很有效,部分原因是强调不变性。
  • 当您访问可变共享数据时,使用locking进行读取和写入。
  • 除非你真的需要,否则不要试图locking。 锁是昂贵的,但很less是瓶颈。
  • Monitor.Wait应该几乎总是一个条件循环的一部分,等待一个条件变为真,如果不是,则等待。
  • 尽量避免持有锁超过您的需要。
  • 如果您需要一次获得两把锁,请彻底logging下订单,并确保始终使用相同的订单。
  • logging您的types的线程安全性。 大多数types不需要是线程安全的,只需要不是线程敌对的(即“你可以从多个线程使用它们,但是如果你想共享它们, 你有责任去取出锁)
  • 不要从非UI线程访问UI(除了已logging的线程安全方式)。 在Windows窗体中,使用Control.Invoke / BeginInvoke

这是我的头顶 – 如果这对你有用,我可能会想更多,但如果不是这样,我会停下来。

学习正确编写multithreading程序是非常困难和耗时的。

所以第一步是:用一个根本不使用multithreading的实现replace。

然后,当你发现了一些非常简单的安全的方法时,只要你发现真正的需要,就仔细地把线程回复。 可靠工作的非线程实现要比破坏的线程实现好得多。

当您准备开始时,请使用线程安全队列在线程之间传输工作项的devise,并注意确保这些工作项一次只能由一个线程访问。

尽量避免在代码周围喷洒lock块,希望它能成为线程安全的。 它不起作用。 最终,两条代码path将以不同的顺序获取相同的锁,一切都将停止(每两周一次,在客户的服务器上)。 如果将线程与触发事件组合在一起,并且在您触发事件时您持有锁,则处理器可能会取出另一个锁,而现在您将按特定顺序持有一对锁。 如果在其他情况下以相反的顺序被拿走呢?

总而言之,这是一个非常艰巨的课题,我认为在简短的回答中提出几点build议可能会产生误导,并说“你走吧!” – 我相信这不是许多学者在这里给出答案的意图,但这是许多人从总结的build议中得到的印象。

相反, 购买这本书 。

这是一个非常好的措辞摘要从这个网站 :

multithreading也有缺点。 最大的问题是它可能导致更复杂的程序。 拥有多个线程本身并不会造成复杂性; 线程之间的交互会产生复杂性。 无论这种相互作用是否是故意的,这都可以适用,并且可能导致较长的开发周期,以及持续存在间歇性和不可重复性错误的敏感性。 出于这个原因,在multithreadingdevise中保持这种交互很简单 – 或者根本不使用multithreading – 除非你有一个特别的重写和debugging的愿望!

来自Stroustrup的完美总结 :

传统的处理并发性的方法是通过让一堆线程在单个地址空间中松动,然后使用锁来尝试处理由此产生的数据竞争和协调问题,这在正确性和可理解性方面可能是最差的。

(像Jon Skeet,其中大部分假设.NET)

冒着看似争议的风险,像这样的评论只是困扰着我:

学习正确编写multithreading程序是非常困难和耗时的。

应尽可能避免线程…

实际上不可能写一些软件来做一些有意义的事情,而不是在某些能力上利用线程。 如果您在Windows上,请打开您的任务pipe理器,启用线程计数列,并且您可能一方面计算使用单个线程的进程的数量。 是的,不应该为了使用线程而简单地使用线程,也不应该这样做,但坦率地说,我相信这些陈词滥调是经常使用的。

如果我必须为真正的新手烧掉multithreading编程,我会这样说:

  • 在跳入它之前,首先要明白类边界与线程边界不一样。 例如,如果您的类的callback方法被另一个线程(例如,对TcpListener.BeginAcceptTcpClient()方法的AsyncCallback委托)调用,请理解该callback该另一个线程执行。 所以,即使callback发生在同一个对象上,你仍然需要在callback方法中同步访问对象的成员。 线程和类是正交的; 理解这一点很重要。
  • 确定哪些数据需要在线程之间共享。 一旦定义了共享数据,如果可能的话,尝试将其合并到一个类中。
  • 限制可以写入和读取共享数据的地方。 如果你能把这件事写到一个地方写作和一个地方阅读,你会自己做一个巨大的好处。 这并不总是可能的,但是射击是一个不错的目标。
  • 显然,请确保使用Monitor类或lock关键字来同步对共享数据的访问。
  • 如果可能,请使用单个对象来同步共享数据,而不pipe有多less个不同的共享字段。 这将简化事情。 但是,它也可能过度限制事物,在这种情况下,您可能需要每个共享字段的同步对象。 在这一点上,使用不可变的类变得非常方便。
  • 如果你有一个线程需要发送另一个线程的信号,我强烈build议使用ManualResetEvent类来完成这个工作,而不是使用事件/委托。

总而言之,我认为线程化并不困难,但可能是单调乏味的。 尽pipe如此,一个正确的线程应用程序将更加灵敏,您的用户将是最感激的。

编辑:关于ThreadPool.QueueUserWorkItem(),asynchronous委托,C#中的各种BeginXXX / EndXXX方法对等,没有什么“极其困难”。 如果有的话,这些技术使得以线程化的方式完成各种任务变得更容易。 如果您有一个GUI应用程序执行任何繁重的数据库,套接字或I / O交互操作,那么实际上不可能在不使用幕后线程的前提下响应用户。 我上面提到的技术使这成为可能,并且可以轻松使用。 可以肯定的是,理解这些陷阱是很重要的。 我只是相信我们做程序员,特别是年轻的程序员,当我们谈论“非常困难”的multithreading编程是什么或者应该怎样“避免”线程时,是一种伤害。 像这样的评论过分简化了这个问题,夸大了神话,事实是线程从未如此简单。 有合法的理由使用线程,这样的陈词滥调似乎适得其反。

你可能对CSP感兴趣,或者处理并发的其他理论代数。 大多数语言都有CSP库,但是如果语言不是为它devise的,那么需要一点训练才能正确使用。 但最终,每一种并发/线程都归结为一些相当简单的基础:避免共享可变数据,并且准确地理解每个线程在等待另一个线程时可能不得不阻塞的时间和原因。 (在CSP中,共享数据根本不存在,每个线程(或CSP术语中的进程) 允许通过阻塞消息传递通道与其他进行通信,由于没有共享数据,竞争条件就会消失。被阻塞,就很容易推理同步,从字面上certificate不会发生死锁。)

另一个很好的方法是将现有代码更容易地加以改进,即为系统中的每个锁分配一个优先级或级别,并确保遵循以下规则:

  • 在N级locking时,只能获得较低级别的新锁
  • 必须同时获取同一级别的多个锁,作为单个操作,它总是试图以相同的全局顺序获取所有请求的锁(注意任何一致的顺序都可以,但是任何试图获取或更多的级别为N的锁,必须按照任何其他线程在代码中其他任何地方执行的顺序来获取它们。

遵循这些规则意味着发生死锁是根本不可能的。 那么你只需要担心可变的共享数据。

大力强调乔恩发表的第一点。 你拥有的更不可变的状态(即:全局variables是const等等),你的生活会变得越容易(即:你必须处理的锁越less,你将得到的推理就越less做交错命令等等)

另外,如果你有多个线程需要访问的小对象,通常情况下你最好在线程之间复制它,而不是拥有一个共享的,可变的全局variables,你必须持有一个锁来读/变异。 这是你的理智和记忆效率之间的权衡。

处理线程时寻找devise模式是最好的方法。 很多人不尝试,而试图自行实现更less或更复杂的multithreading构造。

我可能会同意迄今发布的所有意见。 此外,我build议使用一些现有的更粗粒度的框架,提供构build模块,而不是简单的设施,如锁或等待/通知操作。 对于Java而言,它将仅仅是内置的java.util.concurrent包,它为您提供了可以轻松组合以实现multithreading应用程序的现成的类。 这样做的一大好处就是避免了编写低级操作,导致代码难以阅读且容易出错,有利于更清晰的解决scheme。

根据我的经验,似乎大多数并发问题可以通过使用这个包在Java中解决。 但是,当然,你总是应该小心multithreading,无论如何,这是有挑战性的。

添加到其他人已经在这里做的点:


一些开发者似乎认为“几乎足够”的locking就足够了。 根据我的经验,相反的情况可能是真的 – “几乎可以”locking可能比locking更糟

设想线程一个locking资源R,使用它,然后解锁它。 A然后使用资源R'而不locking。

同时,线程B尝试访问R而A被locking。 线程B被阻塞直到线程A解锁R. 然后,CPU上下文切换到线程B,线程B访问R,然后在其时间片期间更新R' 。 该更新呈现R与R不一致,导致A尝试访问它时失败。


尽可能多地testing不同的硬件和操作系统架构。 不同的CPUtypes,不同数量的内核和芯片,Windows / Linux / Unix等


第一个使用multithreading程序的开发者是一个叫Murphy的人。

那么到目前为止,每个人都是以Windows / .NET为中心的,所以我会用一些Linux / C.

不惜一切代价 (PDF) 避免futexes ,除非你确实需要恢复一些与互斥锁相关的时间。 我现在用Linux futexes拉我的头发。

我还没有勇气去实际的无锁解决scheme ,但我正在从纯粹的挫折中迅速接近这一点。 如果我能够find一个好的,有据可查的,可移植的,我可以真正研究和掌握的实现,那么我可能会完全抛弃这些线程。

我最近遇到过这么多的代码,它们使用的线程实际上不应该这样做,很显然,当一个单独的(是的,只有一个)分支可以完成这个工作时,有人只是想expression他们对POSIX线程的永恒的爱。

我希望我能给你一些“正常”,“一直”的代码。 我可以,但是作为一个演示(服务器和每个连接启动线程)将是非常愚蠢的。 在更复杂的事件驱动的应用程序中,我还没有(几年之后)写任何不会遇到几乎不可能复制的神秘并发问题的东西。 所以我是第一个承认,在这种应用中,线程对我来说只是一个太多的绳子。 他们是如此诱人,我总是最终挂在自己身上。

这是可变的状态,愚蠢的

这是Brian Goetz的Java Concurrency in Practice的直接引用。 尽pipe这本书是以Java为中心的,但是“第一部分总结”给出了一些其他有用的提示,这些提示将适用于许multithreading编程环境。 以下是一些相同的总结:

  • 不可变的对象是自动线程安全的。
  • 用锁locking每个可变variables。
  • 从多个线程访问可变variables而不同步的程序是一个破坏的程序。

我会build议拿这本书的副本深入处理这个难题。

替代文字http://www.cs.umd.edu/class/fall2008/cmsc433/jcipMed.jpg

我想跟随Jon Skeet的build议提供更多的技巧:

  • 如果您正在编写“服务器”,并且可能具有大量的插入并行性,请不要使用Microsoft的SQL Compact。 它的锁经理是愚蠢的。 如果你使用SQL Compact,不要使用可序列化的事务(这恰好是TransactionScope类的默认事务)。 事情会迅速崩溃。 SQL Compact不支持临时表,并且当您尝试在序列化事务中模拟它们时,它会显得很愚蠢,比如在_sysobjects表的索引页上使用x锁。 即使你不使用临时表,它也会非常渴望locking升级。 如果你需要串行访问多个表,最好的办法是使用可重复读取事务(以提供primefaces性和完整性),然后实现基于域对象(帐户,客户,交易等)的自己的层次lockingpipe理器,而不是使用数据库的基于页面的行表的scheme。

    但是,当你这样做时,你需要小心(像John Skeet所说的)来创build一个定义好的locking层次结构。

  • 如果您创build了自己的锁pipe理器,请使用<ThreadStatic>字段来存储关于您所使用的锁的信息,然后在锁pipe理器内的每个位置添加断言来强制执行锁层次结构规则。 这将有助于预先解决潜在的问题。

  • 在UI线程中运行的任何代码中,在!InvokeRequired (用于winforms)或Dispatcher.CheckAccess() (用于WPF)上添加断言。 您应该类似地将反向断言添加到在后台线程中运行的代码。 这样一来,看着一个方法的人就会知道,只要看看它的线程要求是什么。 断言也将有助于发现错误。

  • 声称疯狂,即使在零售版本。 (这意味着投掷,但你可以让你的投掷看起来像断言)。 一个崩溃转储的例外说“你违反了线程规则”,以及堆栈跟踪,更容易debugging,然后来自世界另一端的客户的报告说:“每隔一段时间,应用程序只是冻结在我身上,或者它吐出来gobbly gook“。

而不是locking容器,你应该使用ReaderWriterLockSlim。 这给你像locking数据库 – 无限数量的读者,一个作家,和升级的可能性。

至于devise模式,pub / sub是非常完善的,并且很容易在.NET中编写(使用readerwriterlockslim)。 在我们的代码中,我们有一个每个人都可以获得的MessageDispatcher对象。 您订阅它,或者以完全asynchronous的方式发送消息。 所有你必须locking的是注册的function和他们工作的任何资源。 它使multithreading更容易。