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