今天我不得不修复一些使用线程的旧VB.NET 1.0代码.问题是从工作线程而不是UI线程更新UI元素.我花了一些时间才发现我可以使用InvokeRequired的断言来查找问题.
除了上面提到的并发修改问题,还有可能遇到的死锁,竞争条件等.由于调试/修复线程问题很痛苦,我想知道如何减少这个领域的编码错误/错误以及如何更容易地找到它们.所以,我要求的是:
编写多线程代码时是否有任何好的模式?什么是Dos和Don'ts?
您使用什么技术来调试线程问题?
如果适用且可能,请提供一些示例代码.答案应该与.NET框架(任何版本)相关.
这可能是一个庞大的列表 - 阅读Joe Duffy的优秀" Windows上的并发编程 "以获取更多细节.这几乎是一个大脑转储......
在拥有锁时,尽量避免调用大量代码
避免锁定类外部代码也可能锁定的引用
如果您需要一次获得多个锁,请始终以相同的顺序获取这些锁
在合理的情况下,使用不可变类型 - 它们可以在线程之间自由共享
除了不可变类型之外,尽量避免在线程之间共享数据
避免尝试使你的类型线程安全; 大多数类型都不需要,通常需要共享数据的代码需要控制锁定本身
在WinForms应用程序中:
不要在UI线程上执行任何长时间运行或阻塞操作
不要从UI线程以外的任何线程触摸UI.(使用BackgroundWorker,Control.Invoke/BeginInvoke)
尽可能避免线程局部变量(也称为线程静态) - 它们可能导致意外行为,特别是在ASP.NET上,其中请求可能由不同的线程提供服务(搜索"线程敏捷性"和ASP.NET)
不要试图聪明.无锁并发代码是巨大的难以得到的权利.
记录类型的线程模型(和线程安全性)
Monitor.Wait应该几乎总是与某种检查一起使用,在while循环中(即while(我无法继续)Monitor.Wait(monitor))
每次使用其中一个时,请仔细考虑Monitor.Pulse和Monitor.PulseAll之间的区别.
插入Thread.Sleep以使问题消失绝不是一个真正的解决方案.
看看"并行扩展"和"协调与并发运行时"作为简化并发的方法.Parallel Extensions将成为.NET 4.0的一部分.
在调试方面,我没有太多建议.使用Thread.Sleep来提高看到竞争条件和死锁的可能性是可行的,但是在你知道把它放到哪里之前你必须对错误有一个合理的理解.记录非常方便,但不要忘记代码进入某种量子状态 - 通过记录来观察它几乎必然会改变它的行为!
我不确定这对你正在使用的特定应用程序有多大帮助,但是这里有两种从编写多线程代码的函数式编程中借用的方法:
不可变的物体
如果需要在线程之间共享状态,则状态应该是不可变的.如果一个线程需要对对象进行更改,它会使用更改创建对象的全新版本,而不是改变对象的状态.
不可变性本身并不限制您可以编写的代码类型,也不会低效.有许多不可变堆栈的实现,构成映射和集合基础的各种不可变树,以及其他类型的不可变数据结构,并且许多(如果不是全部)不可变数据结构与它们的可变对应物一样有效.
由于对象是不可变的,因此一个线程不可能在您的鼻子下改变共享状态.这意味着您不需要获取锁来编写多线程代码.这种方法消除了与死锁,活锁和竞争条件相关的一整类错误.
Erlang风格的消息传递
您不需要学习该语言,但请查看Erlang以了解它如何实现并发.Erlang应用程序可以无限扩展,因为每个进程都与其他进程完全分离(注意:这些不完全是进程,但也不完全是线程).
进程启动并简单地旋转循环等待消息:消息以元组的形式接收,然后进程可以模式匹配以查看消息是否有意义.进程可以发送其他消息,但它们对接收消息的人无动于衷.
这种风格的优点是消除锁定,当一个进程失败时它不会导致整个应用程序崩溃.以下是Erlang风格并发的一个很好的总结:http://www.defmacro.org/ramblings/concurrency.html