【问题标题】:Threading Best Practices线程最佳实践
【发布时间】:2010-10-14 05:19:35
【问题描述】:

我从事的许多项目的线程实现都很差,而我是必须追踪这些的傻瓜。是否有公认的最佳方式来处理线程。我的代码总是在等待一个永远不会触发的事件。

我有点像设计模式之类的想法。

【问题讨论】:

  • 您能具体谈谈您的问题吗?

标签: multithreading


【解决方案1】:

这是可变状态,笨蛋

这是 Brian Goetz Java Concurrency in Practice 的直接引述。尽管本书以 Java 为中心,但“第一部分摘要”提供了一些其他有用的提示,这些提示将适用于许多线程编程上下文。以下是同一摘要中的更多内容:

  • 不可变对象自动是线程安全的。
  • 用锁保护每个可变变量。
  • 从多个线程访问可变变量的程序没有 同步是一个损坏的程序。

我建议您购买这本书的副本,以深入了解这个难题。


(来源:umd.edu

【讨论】:

    【解决方案2】:

    (假设 .NET;类似的事情也适用于其他平台。)

    嗯,有 很多 的事情需要考虑。我建议:

    • 不变性非常适合多线程。函数式编程同时运行良好,部分原因在于它强调不变性。
    • 在访问可变共享数据时使用锁,包括读取和写入。
    • 除非确实需要,否则不要尝试解锁。锁很昂贵,但很少成为瓶颈。
    • Monitor.Wait 几乎应该始终成为条件循环的一部分,等待条件变为真,如果不是,则再次等待。
    • 尽量避免持有锁的时间过长。
    • 如果您需要一次获得两把锁,请彻底记录订购并确保始终使用相同的顺序。
    • 记录类型的线程安全性。大多数类型不需要需要是线程安全的,它们只需要不是线程敌对的(即“你可以从多个线程中使用它们,但是你的责任是如果您想共享锁,请取出锁)
    • 不要从非 UI 线程访问 UI(以记录的线程安全方式除外)。在 Windows 窗体中,使用 Control.Invoke/BeginInvoke

    这不是我的想法 - 如果这对你有用,我可能会想更多,但我会停在那里以防万一。

    【讨论】:

    • 一个非常好的列表。也许像“尽量避免在持有锁时调用外部代码”这样的观点。也应该添加。
    • 列表顶部应该是 - 除非确实需要,否则不要使其成为多线程。其次应该是 - 保持线程简单,例如长时间运行的任务在单个后台线程上运行,以便 UI 保持响应。
    • 我想你忘了添加(我想你已经在别处说过了)——如果你正在编写一个 Web 应用程序 (ASP.NET),除非你真的需要,否则不要使用显式线程到。让服务器为您处理单独的请求。否则,它不会扩展。
    • 我的 Evernote 中有一个来自 C/C++ 预期的类似列表。实际上语言的作用很小
    【解决方案3】:

    我想按照 Jon Skeet 的建议提供更多提示:

    • 如果您正在编写“服务器”,并且可能具有大量插入并行性,请不要使用 Microsoft 的 SQL Compact。它的锁管理器是愚蠢的。如果您确实使用 SQL Compact,请不要使用可序列化事务(这恰好是 TransactionScope 类的默认设置)。事情很快就会在你身上分崩离析。 SQL Compact 不支持临时表,当您尝试在序列化事务中模拟它们时,它会做一些非常愚蠢的事情,例如在 _sysobjects 表的索引页上使用 x 锁。即使您不使用临时表,它也非常渴望锁定提升。如果您需要对多个表进行串行访问,最好的办法是使用可重复读取事务(以提供原子性和完整性),然后根据域对象(帐户、客户、事务等)实现您自己的分层锁管理器,而不是使用数据库的基于页行表的方案。

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

    • 1234563这将有助于提前根除潜在问题。
    • 在 UI 线程中运行的任何代码中,在 !InvokeRequired(对于 winforms)或 Dispatcher.CheckAccess()(对于 WPF)上添加断言。您应该类似地将反向断言添加到在后台线程中运行的代码中。这样,查看方法的人只需查看它就知道它的线程要求是什么。断言还有助于捕获错误。

    • 即使在零售版本中,也可以疯狂断言。 (这意味着投掷,但你可以让你的投掷看起来像断言)。与堆栈跟踪一起显示“您这样做违反了线程规则”的异常的故障转储比世界另一端的客户报告“时不时应用程序”更容易调试只是冻结在我身上,或者它吐出狼吞虎咽的东西”。

    【讨论】:

      【解决方案4】:

      学习正确编写多线程程序非常困难且耗时。

      所以第一步是:将实现替换为根本不使用多线程的实现。

      然后,当且仅当您发现真正需要线程时,当您找到一些非常简单的安全方法时,小心地将线程放回原处。可靠地工作的非线程实现远好于损坏的线程实现。

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

      尽量避免只是在代码周围喷洒lock 块,希望它会变得线程安全。它不起作用。最终,两个代码路径将以不同的顺序获取相同的锁,一切都会停止(每两周一次,在客户的服务器上)。如果您将线程与触发事件结合起来,并且在触发事件时持有锁,则这种情况尤其可能发生 - 处理程序可能会取出另一个锁,现在您有一对按特定顺序持有的锁。如果在其他情况下以相反的顺序取出它们怎么办?

      简而言之,这是一个如此庞大而困难的主题,我认为在简短的回答中给出一些指示并说“你走吧!”可能会产生误导。 - 我敢肯定,这不是许多博学的人在这里给出答案的意图,但这是许多人从总结建议中得到的印象。

      改为buy this book

      这是一个措辞非常优美的摘要from this site

      多线程也自带 缺点。最大的是它 可能导致复杂得多 程式。拥有多个线程确实 本身不会产生复杂性;它是 线程之间的交互 这会产生复杂性。这适用 交互是否 故意的,并且可能导致长期 开发周期,以及 持续的间歇性易感性 和不可重现的错误。为了这 原因,保持这样是值得的 多线程设计中的交互 简单——或者不使用多线程 全部——除非你有一个特殊的 喜欢重写和调试!

      Perfect summary from Stroustrup:

      处理并发的传统方式是让一堆 线程在单个地址空间中松散,然后使用锁尝试 应对由此产生的数据竞争和协调问题是 就正确性和 可理解性。

      【讨论】:

      • 看起来是一个普遍的问题,即对好的 cmets 投反对票。我猜有人有愤怒问题。
      • 我没有投反对票,但也许有些人觉得“用根本不使用多线程的实现替换”有点回避这个问题?
      • 不反对,因为您的回答部分正确,但是应该从一开始就构建多线程程序。 “我们稍后再添加线程”就像在后面添加任何核心功能一样愚蠢,比从一开始就做更危险,甚至更多的工作
      • @Robert Gould - 我完全同意。多线程设计将是对早期设计的全面重构。 “附加”方法通常会导致锁块散布。
      • @Earwicker:我经常对自己经历和看到的反对票感到困惑。这只是一种媒介,有些人会以错误的方式对某些东西投反对票。
      【解决方案5】:

      (和 Jon Skeet 一样,其中大部分假设是 .NET)

      冒着显得争论不休的风险,像这样的 cmets 只会打扰我:

      学习编写多线程 正确的程序是非常 困难且耗时。

      在以下情况下应避免使用线程 可能...

      如果不利用某些容量的线程,几乎不可能编写出能做任何重要事情的软件。如果您在 Windows 上,打开任务管理器,启用线程计数列,您可能一方面可以计算使用单个线程的进程数。是的,不应该为了使用线程而简单地使用线程,也不应该漫不经心地做,但坦率地说,我认为这些陈词滥调被使用得太频繁了。

      如果我不得不为真正的新手简化多线程编程,我会这样说:

      • 在进入它之前,首先要了解类边界与线程边界不同。例如,如果您的类上的回调方法被另一个线程调用(例如,TcpListener.BeginAcceptTcpClient() 方法的 AsyncCallback 委托),请了解回调在该其他线程上执行 。所以即使回调发生在同一个对象上,你仍然必须在回调方法中同步访问对象的成员。线程和类是正交的;理解这一点很重要。
      • 确定哪些数据需要在线程之间共享。定义共享数据后,尽可能将其合并到一个类中。
      • 限制可以写入和读取共享数据的位置。如果你能把它归结为一个写作的地方和一个阅读的地方,你会帮自己一个巨大的忙。这并不总是可能的,但这是一个不错的目标。
      • 显然,请确保使用 Monitor 类或 lock 关键字同步对共享数据的访问。
      • 如果可能,请使用单个对象来同步您的共享数据,而不管有多少不同的共享字段。这将简化事情。但是,它也可能过度约束事物,在这种情况下,您可能需要为每个共享字段创建一个同步对象。在这一点上,使用不可变类变得非常方便。
      • 如果您有一个线程需要向另一个线程发出信号,我强烈建议您使用 ManualResetEvent 类来执行此操作,而不是使用事件/委托。

      总而言之,我想说线程并不难,但它可能很乏味。不过,正确线程化的应用程序会响应更快,您的用户也会非常感激。

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

      【讨论】:

      • 我喜欢它——人们经常说某件事(因为它是真的),这意味着我们可以称之为“陈词滥调”!
      • 内存隔离(即不是线程)会更好,以防止程序员在错误的线程中意外使用数据。虚拟机实际上应该能够将数据状态标记为不可访问、只读、线程锁定等,至少在调试模式下是这样。那会很有帮助。
      【解决方案6】:

      您应该使用 ReaderWriterLockSlim,而不是锁定容器。这为您提供了类似锁定的数据库 - 无限数量的读取器、一个写入器以及升级的可能性。

      至于设计模式,pub/sub 已经非常成熟,并且很容易在 .NET 中编写(使用 readerwriterlockslim)。在我们的代码中,我们有一个每个人都可以获得的 MessageDispatcher 对象。您订阅它,或者以完全异步的方式发送消息。您只需要锁定已注册的函数和它们使用的任何资源。它使多线程变得更加容易。

      【讨论】:

        【解决方案7】:

        在处理线程时寻找设计模式是最好的开始方法。可惜很多人不尝试,而是尝试自己实现更简单或更复杂的多线程构造。

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

        根据我的经验,使用这个包似乎可以在 Java 中解决大多数并发问题。但是,当然,您始终应该小心使用多线程,无论如何它都是具有挑战性的。

        【讨论】:

          【解决方案8】:

          嗯,到目前为止,每个人都以 Windows / .NET 为中心,所以我将加入一些 Linux / C。

          Avoid futexes at all costs(PDF),除非你真的,真的需要恢复一些花在互斥锁上的时间。我目前正在使用 Linux futexes。

          我还没有勇气选择practical lock free solutions,但出于纯粹的挫败感,我正在迅速接近这一点。如果我能找到一个好的、有据可查的、可移植的实现,我可以真正学习和掌握,我可能会完全放弃线程。

          我最近遇到了很多代码,这些代码使用了实际上不应该使用的线程,很明显,当单个(是的,只有一个)fork 可以完成这项工作时,有人只是想表达他们对 POSIX 线程的永恒热爱。

          我希望我能给你一些“正常工作”、“一直有效”的代码。我可以,但是用作演示(服务器等为每个连接启动线程)会很愚蠢。在更复杂的事件驱动应用程序中,我还没有(几年后)编写任何不遭受几乎不可能重现的神秘并发问题的东西。所以我是第一个承认,在那种应用程序中,线程对我来说有点太多了。它们太诱人了,我最后总是上吊。

          【讨论】:

          • 外汇:如果您有线程从队列中拉出多个工作项,请为每个线程拉出不同的数字,这样它们就不会同时返回做更多工作。
          • 奇怪,评论框抹去了我评论的第一行。我说,futex 只有在满足时才会变慢。良好的线程设计可以避免争用。
          【解决方案9】:

          您可能对CSP 之类的东西感兴趣,或者对处理并发性的其他理论代数之一感兴趣。大多数语言都有 CSP 库,但如果该语言不是为它设计的,则需要一些纪律才能正确使用。但最终,每种并发/线程都归结为一些相当简单的基础知识:避免共享可变数据,并准确了解每个线程在等待另一个线程时何时以及为何必须阻塞。 (在 CSP 中,共享数据根本不存在。每个线程(或 CSP 术语中的进程)允许通过阻塞消息传递通道与其他线程通信。由于没有共享数据,因此竞争条件消失了。由于消息传递是阻塞的,因此很容易推断同步,并从字面上证明不会发生死锁。)

          另一个更容易改进到现有代码的良好做法是为系统中的每个锁分配优先级或级别,并确保始终遵循以下规则:

          • 在 N 级持有锁时,您 只能获取较低级别的新锁
          • 同一级别的多个锁必须 同时被收购,作为 单个操作,它总是尝试 获取所有请求的锁 相同的全局顺序(请注意,任何 一致的顺序会做,但任何 试图获取一个或的线程 N级有更多锁,必须做 以与任何相同的顺序获取它们 其他线程会在其他任何地方做 在代码中。)

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

          【讨论】:

            【解决方案10】:

            补充其他人已经在这里提出的观点:


            一些开发人员似乎认为“差不多”锁定就足够了。根据我的经验,情况正好相反——“几乎足够”的锁定可能比足够的锁定更糟糕

            想象线程 A 锁定资源 R,使用它,然后解锁它。然后 A 在没有锁的情况下使用资源 R'。

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


            在尽可能多的不同硬件和操作系统架构上进行测试。不同的 CPU 类型、不同的内核和芯片数量、Windows/Linux/Unix 等。


            第一个使用多线程程序的开发人员是一个名叫 Murphy 的人。

            【讨论】:

            • 您还需要小心锁定过多。锁定过多或“处于错误级别”会破坏性能。
            • Jon Skeet(谁?)解决了死锁问题:始终以相同的顺序锁定。 (我发现考虑锁的粒度很有帮助,当需要超过 1 个锁时,总是从大粒度到小粒度。)但要避免过早优化。先锁定,稍后提问。
            【解决方案11】:

            非常强调 Jon 发布的第一点。您拥有的状态越不可变(即:全局变量是 const 等),您的生活就会越轻松(即:您必须处理的锁越少,推理就越少做关于交错顺序等...)

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

            【讨论】:

            • 不仅你的理智,而且通常还有性能——共享对象需要锁定,这会扼杀可伸缩性。复制或不变性的另一种方法是努力确保对象一次仅对一个线程可见,这(如果正确)允许安全、无锁地更改工作项。
            猜你喜欢
            • 1970-01-01
            • 2014-06-04
            • 1970-01-01
            • 1970-01-01
            • 2014-06-14
            • 2010-09-05
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多