我工作的很多项目的线程实现都很差,而且我是那些必须跟踪它们的傻瓜.是否有一种可接受的最佳方式来处理线程.我的代码总是在等待一个永不激活的事件.
我有点像设计模式或其他东西.
(假设.NET;类似的东西适用于其他平台.)
嗯,有很多事情需要考虑.我建议:
不可变性对于多线程非常有用.功能编程同时工作得很好,部分原因是强调不变性.
访问可变共享数据时使用锁,包括读取和写入.
除非你真的需要,否则不要试图锁定.锁是昂贵的,但很少是瓶颈.
Monitor.Wait
应该几乎总是成为条件循环的一部分,等待条件变为真,如果不是则再次等待.
尽量避免长时间握住锁定.
如果您需要一次获得两个锁,请记录订购,并确保始终使用相同的订单.
记录您的类型的线程安全性.大多数类型并不需要是线程安全的,他们只需要不是线程敌对(即"你可以用它们从多个线程,但它是你的责任,采取了锁,如果你想分享)
不要从非UI线程访问UI(以文档化的线程安全方式除外).在Windows窗体中,使用Control.Invoke/BeginInvoke
这是我的头脑 - 我可能会想到更多,如果这对你有用,但我会停在那里,以防它不是.
学习正确编写多线程程序非常困难且耗时.
因此,第一步是:将实现替换为根本不使用多个线程的实现.
然后,当你发现一些非常简单的安全方法的时候,当你发现真正的需要时,小心地将线程重新插入.可靠地工作的非线程实现远比破坏的线程实现好.
当您准备好开始时,支持使用线程安全队列在线程之间传输工作项的设计,并注意确保这些工作项一次只能由一个线程访问.
尽量避免lock
在代码周围喷涂块,希望它能成为线程安全的.它不起作用.最终,两个代码路径将以不同的顺序获取相同的锁,并且所有内容都将停止(每两周一次,在客户的服务器上).如果您将线程与触发事件组合在一起,并且在触发事件时保持锁定,则这种情况尤其可能 - 处理程序可能会取出另一个锁定,现在您按特定顺序拥有一对锁定.如果在其他情况下它们以相反的顺序被取出怎么办?
简而言之,这是一个如此庞大而困难的主题,我认为在一个简短的回答中提出一些指示并说"你走了!"可能会产生误导. - 我确信这里并没有很多有学问的人给出答案的意图,但这是许多人从总结建议中得到的印象.
相反,买这本书.
这是一个来自这个网站的措辞非常好的摘要:
多线程也有缺点.最大的问题是它可以导致更复杂的程序.拥有多个线程本身并不会产生复杂性; 它是创建复杂性的线程之间的交互.这适用于相互作用是否是有意的,并且可能导致长的开发周期,以及对间歇性和不可再现的错误的持续敏感性.出于这个原因,在多线程设计中保持这种交互很简单 - 或者根本不使用多线程 - 除非你有一种特殊的重写和调试倾向!
Stroustrup的完美总结:
通过让一堆线程在单个地址空间中松散然后使用锁来尝试处理由此产生的数据争用和协调问题来处理并发性的传统方法在正确性和可理解性方面可能是最糟糕的.
(就像Jon Skeet,其中大部分假设是.NET)
看似有争议的风险,像这样的评论只是困扰我:
学习正确编写多线程程序非常困难且耗时.
应尽可能避免线程......
如果不利用某些容量的线程,编写任何有意义的软件几乎是不可能的.如果您在Windows上,打开任务管理器,启用"线程计数"列,您可以一方面指望使用单个线程的进程数.是的,不应该仅仅为了使用线程而使用线程,也不应该使用线程,但坦率地说,我相信这些陈词滥调经常被使用.
如果我不得不为真正的新手煮多线程编程,我会说:
在进入它之前,首先要了解类边界与线程边界不同.例如,如果您的类上的回调方法被另一个线程调用(例如,对TcpListener.BeginAcceptTcpClient()方法的AsyncCallback委托),请理解该回调在该另一个线程上执行.因此,即使回调发生在同一个对象上,您仍然必须在回调方法中同步对对象成员的访问.线程和类是正交的; 理解这一点很重要.
确定线程之间需要共享的数据.定义共享数据后,尝试将其合并到单个类中(如果可能).
限制可以写入和读取共享数据的位置.如果你可以把这个写到一个地方写作和一个阅读的地方,你将为自己做一个巨大的帮助.这并不总是可行,但这是一个很好的目标.
显然,请确保使用Monitor类或lock关键字同步对共享数据的访问.
如果可能,使用单个对象来同步共享数据,而不管有多少个不同的共享字段.这将简化事情.但是,它也可能过度约束事物,在这种情况下,您可能需要为每个共享字段提供同步对象.在这一点上,使用不可变类变得非常方便.
如果你有一个线程需要发出另一个线程的信号,我强烈建议使用ManualResetEvent类来执行此操作而不是使用事件/委托.
总而言之,我会说线程并不困难,但它可能很乏味.仍然,正确的线程应用程序将更具响应性,您的用户将非常感激.
编辑:在C#中,ThreadPool.QueueUserWorkItem(),异步委托,各种BeginXXX/EndXXX方法对等都没有"极其困难".如果有的话,这些技术可以更容易地以线程方式完成各种任务.如果您有一个GUI应用程序可以执行任何繁重的数据库,套接字或I/O交互,则实际上不可能在不利用幕后线程的情况下使前端响应用户.我上面提到的技术使这成为可能,并且使用起来轻而易举.当然,了解陷阱是很重要的.我只是相信我们做程序员,特别是年轻人,当我们谈论"非常困难"的多线程编程是什么时,或者应该如何"避免"线程时,这是一种损害.像这样的评论过于简化问题并夸大了神话,因为事实是线程从未如此简单.有正当理由使用线程,这样的陈词滥调对我来说似乎适得其反.
您可能对CSP或其他用于处理并发的理论代数之一感兴趣.大多数语言都有CSP库,但如果语言不是为它设计的,那么正确使用它需要一些规范.但最终,每种并发/线程都归结为一些相当简单的基础:避免共享可变数据,并准确理解每个线程在等待另一个线程时可能必须阻塞的时间和原因.(在CSP中,共享数据根本不存在.每个线程(或CSP术语中的进程)只允许通过阻塞消息传递通道与其他人通信.由于没有共享数据,竞争条件就会消失.阻塞,很容易推理同步,并逐字地证明不会发生死锁.)
另一个更容易改装到现有代码中的好习惯是为系统中的每个锁分配优先级或级别,并确保始终遵循以下规则:
在N级锁定时,您可能只获得较低级别的新锁
必须同时获取同一级别的多个锁,作为单个操作,它始终尝试以相同的全局顺序获取所有请求的锁(请注意,任何一致的顺序都可以,但任何尝试获取一个的线程)或者更多级别为N的锁,必须按照与代码中其他任何线程相同的顺序获取它们.)
遵循这些规则意味着发生死锁根本不可能.然后你只需要担心可变共享数据.