线程最佳实践

时间:2009-03-19 00:28:43

标签: multithreading

我工作的很多项目的线程实现都很差,而且我是必须跟踪这些项目的傻瓜。是否有一种可接受的最佳方式来处理线程。我的代码总是在等待一个永不激活的事件。

我有点像设计模式或其他东西。

11 个答案:

答案 0 :(得分:66)

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

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

  • 多线程的不变性很好。功能编程同时运作良好,部分原因是强调不变性。
  • 访问可变共享数据时使用锁,包括读取和写入。
  • 除非你真的需要,否则不要试图锁定。锁是昂贵的,但很少是瓶颈。
  • Monitor.Wait几乎总是成为条件循环的一部分,等待条件变为真,如果不是则再次等待。
  • 尽量避免长时间按住锁定。
  • 如果您需要一次购买两把锁,请仔细记录订购,并确保始终使用相同的订单。
  • 记录您的类型的线程安全性。大多数类型需要是线程安全的,它们只需要不是线程敌对(即“你可以从多个线程使用它们,但它是你的责任到如果你想分享它们,请拿出锁)。
  • 不要从非UI线程访问UI(以文档化的线程安全方式除外)。在Windows窗体中,使用Control.Invoke / BeginInvoke

这是我的头脑 - 我可能会想到更多,如果这对你有用,但我会停在那里以防它不是。

答案 1 :(得分:33)

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

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

如果你发现了一些非常简单的安全方法,那么当你发现真正的需要时,小心地将线程重新插入。可靠运行的非线程实现远比破坏的线程实现好。

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

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

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

相反,buy this book

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

  

多线程也随之而来   缺点。最大的是它   会导致更加复杂   程式。有多个线程吗   本身并不会造成复杂性;它的   线程之间的交互   这会产生复杂性。这适用   是否相互作用   故意的,并且可以导致很长时间   开发周期,以及   持续的间歇性易感性   和不可重现的错误。为了这   原因是,保持这样做是值得的   多线程设计中的交互   简单 - 或不使用多线程   所有 - 除非你有一个特殊的   喜欢重写和调试!

Perfect summary from Stroustrup

  

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

答案 2 :(得分:14)

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

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

  

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

     

当应该避免线程   可能...

在不利用某些容量的线程的情况下编写具有重要意义的软件几乎是不可能的。如果您在Windows上,打开任务管理器,启用“线程计数”列,您可以一方面指望使用单个线程的进程数。是的,不应该仅仅为了使用线程而使用线程,也不应该使用线程,但坦率地说,我相信这些陈词滥调经常被使用。

如果我不得不为真正的新手煮多线程编程,我会说:

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

总而言之,我会说线程并不困难,但它可能很乏味。尽管如此,正确的线程应用程序将更具响应性,您的用户将非常感激。

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

答案 3 :(得分:6)

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

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

  • 在N级锁定时,你 可能只会获得较低级别的新锁
  • 同一级别的多个锁必须 同时获得,作为一个 单一操作,总是尝试 获取所有请求的锁 相同的全球秩序(注意任何 一致的订单会做,但任何 尝试获取一个或的线程 在N级更多的锁,必须这样做 以任何顺序获得它们 其他线程会在其他地方做 在代码中。)

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

答案 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为中心的,但“第一部分摘要”给出了一些其他有用的提示,这些提示将适用于许多线程编程上下文。以下是同一摘要中的一些内容:

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

我建议您获取该书的副本,以深入处理这一难题。

alt text
(来源:umd.edu

答案 10 :(得分:0)

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

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