【发布时间】:2010-10-14 05:19:35
【问题描述】:
我从事的许多项目的线程实现都很差,而我是必须追踪这些的傻瓜。是否有公认的最佳方式来处理线程。我的代码总是在等待一个永远不会触发的事件。
我有点像设计模式之类的想法。
【问题讨论】:
-
您能具体谈谈您的问题吗?
标签: multithreading
我从事的许多项目的线程实现都很差,而我是必须追踪这些的傻瓜。是否有公认的最佳方式来处理线程。我的代码总是在等待一个永远不会触发的事件。
我有点像设计模式之类的想法。
【问题讨论】:
标签: multithreading
这是可变状态,笨蛋
这是 Brian Goetz Java Concurrency in Practice 的直接引述。尽管本书以 Java 为中心,但“第一部分摘要”提供了一些其他有用的提示,这些提示将适用于许多线程编程上下文。以下是同一摘要中的更多内容:
- 不可变对象自动是线程安全的。
- 用锁保护每个可变变量。
- 从多个线程访问可变变量的程序没有 同步是一个损坏的程序。
我建议您购买这本书的副本,以深入了解这个难题。
(来源:umd.edu)
【讨论】:
(假设 .NET;类似的事情也适用于其他平台。)
嗯,有 很多 的事情需要考虑。我建议:
Monitor.Wait 几乎应该始终成为条件循环的一部分,等待条件变为真,如果不是,则再次等待。这不是我的想法 - 如果这对你有用,我可能会想更多,但我会停在那里以防万一。
【讨论】:
我想按照 Jon Skeet 的建议提供更多提示:
如果您正在编写“服务器”,并且可能具有大量插入并行性,请不要使用 Microsoft 的 SQL Compact。它的锁管理器是愚蠢的。如果您确实使用 SQL Compact,请不要使用可序列化事务(这恰好是 TransactionScope 类的默认设置)。事情很快就会在你身上分崩离析。 SQL Compact 不支持临时表,当您尝试在序列化事务中模拟它们时,它会做一些非常愚蠢的事情,例如在 _sysobjects 表的索引页上使用 x 锁。即使您不使用临时表,它也非常渴望锁定提升。如果您需要对多个表进行串行访问,最好的办法是使用可重复读取事务(以提供原子性和完整性),然后根据域对象(帐户、客户、事务等)实现您自己的分层锁管理器,而不是使用数据库的基于页行表的方案。
但是,当你这样做时,你需要小心(就像 John Skeet 所说的那样)创建一个定义良好的锁层次结构。
在 UI 线程中运行的任何代码中,在 !InvokeRequired(对于 winforms)或 Dispatcher.CheckAccess()(对于 WPF)上添加断言。您应该类似地将反向断言添加到在后台线程中运行的代码中。这样,查看方法的人只需查看它就知道它的线程要求是什么。断言还有助于捕获错误。
即使在零售版本中,也可以疯狂断言。 (这意味着投掷,但你可以让你的投掷看起来像断言)。与堆栈跟踪一起显示“您这样做违反了线程规则”的异常的故障转储比世界另一端的客户报告“时不时应用程序”更容易调试只是冻结在我身上,或者它吐出狼吞虎咽的东西”。
【讨论】:
学习正确编写多线程程序非常困难且耗时。
所以第一步是:将实现替换为根本不使用多线程的实现。
然后,当且仅当您发现真正需要线程时,当您找到一些非常简单的安全方法时,小心地将线程放回原处。可靠地工作的非线程实现远好于损坏的线程实现。
当您准备好开始时,倾向于使用线程安全队列在线程之间传输工作项的设计,并注意确保一次只能由一个线程访问这些工作项。
尽量避免只是在代码周围喷洒lock 块,希望它会变得线程安全。它不起作用。最终,两个代码路径将以不同的顺序获取相同的锁,一切都会停止(每两周一次,在客户的服务器上)。如果您将线程与触发事件结合起来,并且在触发事件时持有锁,则这种情况尤其可能发生 - 处理程序可能会取出另一个锁,现在您有一对按特定顺序持有的锁。如果在其他情况下以相反的顺序取出它们怎么办?
简而言之,这是一个如此庞大而困难的主题,我认为在简短的回答中给出一些指示并说“你走吧!”可能会产生误导。 - 我敢肯定,这不是许多博学的人在这里给出答案的意图,但这是许多人从总结建议中得到的印象。
这是一个措辞非常优美的摘要from this site:
多线程也自带 缺点。最大的是它 可能导致复杂得多 程式。拥有多个线程确实 本身不会产生复杂性;它是 线程之间的交互 这会产生复杂性。这适用 交互是否 故意的,并且可能导致长期 开发周期,以及 持续的间歇性易感性 和不可重现的错误。为了这 原因,保持这样是值得的 多线程设计中的交互 简单——或者不使用多线程 全部——除非你有一个特殊的 喜欢重写和调试!
Perfect summary from Stroustrup:
处理并发的传统方式是让一堆 线程在单个地址空间中松散,然后使用锁尝试 应对由此产生的数据竞争和协调问题是 就正确性和 可理解性。
【讨论】:
(和 Jon Skeet 一样,其中大部分假设是 .NET)
冒着显得争论不休的风险,像这样的 cmets 只会打扰我:
学习编写多线程 正确的程序是非常 困难且耗时。
在以下情况下应避免使用线程 可能...
如果不利用某些容量的线程,几乎不可能编写出能做任何重要事情的软件。如果您在 Windows 上,打开任务管理器,启用线程计数列,您可能一方面可以计算使用单个线程的进程数。是的,不应该为了使用线程而简单地使用线程,也不应该漫不经心地做,但坦率地说,我认为这些陈词滥调被使用得太频繁了。
如果我不得不为真正的新手简化多线程编程,我会这样说:
总而言之,我想说线程并不难,但它可能很乏味。不过,正确线程化的应用程序会响应更快,您的用户也会非常感激。
编辑: 关于 C# 中的 ThreadPool.QueueUserWorkItem()、异步委托、各种 BeginXXX/EndXXX 方法对等,没有什么“非常困难”的地方。如果有的话,这些技术可以大大更轻松地以线程方式完成各种任务。如果您有一个 GUI 应用程序执行任何繁重的数据库、套接字或 I/O 交互,那么实际上不可能在不利用幕后线程的情况下使前端响应用户。我上面提到的技术使这成为可能并且使用起来很容易。可以肯定的是,了解这些陷阱很重要。我只是相信,当我们谈论多线程编程是多么“极其困难”或“应该如何避免”线程时,我们对程序员,尤其是年轻程序员,是一种伤害。当事实是线程从未如此简单时,诸如此类的评论过度简化了问题并夸大了神话。使用线程是有正当理由的,像这样的陈词滥调对我来说似乎适得其反。
【讨论】:
您应该使用 ReaderWriterLockSlim,而不是锁定容器。这为您提供了类似锁定的数据库 - 无限数量的读取器、一个写入器以及升级的可能性。
至于设计模式,pub/sub 已经非常成熟,并且很容易在 .NET 中编写(使用 readerwriterlockslim)。在我们的代码中,我们有一个每个人都可以获得的 MessageDispatcher 对象。您订阅它,或者以完全异步的方式发送消息。您只需要锁定已注册的函数和它们使用的任何资源。它使多线程变得更加容易。
【讨论】:
在处理线程时寻找设计模式是最好的开始方法。可惜很多人不尝试,而是尝试自己实现更简单或更复杂的多线程构造。
我可能会同意迄今为止发布的所有意见。此外,我建议使用一些现有的更粗粒度的框架,提供构建块而不是简单的设施,如锁或等待/通知操作。对于 Java,它只是内置的 java.util.concurrent 包,它为您提供了现成的类,您可以轻松组合以实现多线程应用程序。这样做的最大好处是您可以避免编写低级操作,这会导致难以阅读和容易出错的代码,有利于更清晰的解决方案。
根据我的经验,使用这个包似乎可以在 Java 中解决大多数并发问题。但是,当然,您始终应该小心使用多线程,无论如何它都是具有挑战性的。
【讨论】:
嗯,到目前为止,每个人都以 Windows / .NET 为中心,所以我将加入一些 Linux / C。
Avoid futexes at all costs(PDF),除非你真的,真的需要恢复一些花在互斥锁上的时间。我目前正在使用 Linux futexes。
我还没有勇气选择practical lock free solutions,但出于纯粹的挫败感,我正在迅速接近这一点。如果我能找到一个好的、有据可查的、可移植的实现,我可以真正学习和掌握,我可能会完全放弃线程。
我最近遇到了很多代码,这些代码使用了实际上不应该使用的线程,很明显,当单个(是的,只有一个)fork 可以完成这项工作时,有人只是想表达他们对 POSIX 线程的永恒热爱。
我希望我能给你一些“正常工作”、“一直有效”的代码。我可以,但是用作演示(服务器等为每个连接启动线程)会很愚蠢。在更复杂的事件驱动应用程序中,我还没有(几年后)编写任何不遭受几乎不可能重现的神秘并发问题的东西。所以我是第一个承认,在那种应用程序中,线程对我来说有点太多了。它们太诱人了,我最后总是上吊。
【讨论】:
您可能对CSP 之类的东西感兴趣,或者对处理并发性的其他理论代数之一感兴趣。大多数语言都有 CSP 库,但如果该语言不是为它设计的,则需要一些纪律才能正确使用。但最终,每种并发/线程都归结为一些相当简单的基础知识:避免共享可变数据,并准确了解每个线程在等待另一个线程时何时以及为何必须阻塞。 (在 CSP 中,共享数据根本不存在。每个线程(或 CSP 术语中的进程)仅允许通过阻塞消息传递通道与其他线程通信。由于没有共享数据,因此竞争条件消失了。由于消息传递是阻塞的,因此很容易推断同步,并从字面上证明不会发生死锁。)
另一个更容易改进到现有代码的良好做法是为系统中的每个锁分配优先级或级别,并确保始终遵循以下规则:
遵循这些规则意味着根本不可能发生死锁。然后你只需要担心可变的共享数据。
【讨论】:
补充其他人已经在这里提出的观点:
一些开发人员似乎认为“差不多”锁定就足够了。根据我的经验,情况正好相反——“几乎足够”的锁定可能比足够的锁定更糟糕。
想象线程 A 锁定资源 R,使用它,然后解锁它。然后 A 在没有锁的情况下使用资源 R'。
同时,线程 B 尝试访问 R,而 A 将其锁定。线程 B 被阻塞,直到线程 A 解锁 R。然后 CPU 上下文切换到线程 B,它访问 R,然后在其时间片内更新 R'。该更新使 R' 与 R 不一致,导致 A 尝试访问它时失败。
在尽可能多的不同硬件和操作系统架构上进行测试。不同的 CPU 类型、不同的内核和芯片数量、Windows/Linux/Unix 等。
第一个使用多线程程序的开发人员是一个名叫 Murphy 的人。
【讨论】:
非常强调 Jon 发布的第一点。您拥有的状态越不可变(即:全局变量是 const 等),您的生活就会越轻松(即:您必须处理的锁越少,推理就越少做关于交错顺序等...)
此外,通常情况下,如果您有需要多个线程才能访问的小对象,有时最好在线程之间复制它,而不是拥有一个共享的、可变的全局,您必须持有一个锁才能读取/变异。这是你的理智和记忆效率之间的权衡。
【讨论】: