如何在.NET中编写安全/正确的multithreading代码?

今天我不得不修复一些使用线程的旧VB.NET 1.0代码。 问题是从工作线程而不是UI线程更新UI元素。 我花了一些时间才发现我可以使用InvokeRequired断言来查找问题。

除了上面提到的并发修改问题,还有可能遇到的死锁,竞争条件等。 由于调试/修复线程问题很麻烦,我想知道如何减少这个领域的编码错误/错误以及如何更容易地找到它们。 所以,我要求的是:

  • 编写multithreading代码时是否有任何好的模式? 什么是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来提高看到竞争条件和死锁的可能性是可行的,但是在你知道把它放到哪里之前你必须对错误有一个合理的理解。 记录非常方便,但不要忘记代码进入某种量子状态 – 通过记录来观察它几乎必然会改变它的行为!

我不确定这对你正在使用的特定应用程序有多大帮助,但是这里有两种从编写multithreading代码的函数式编程中借用的方法:

不可变的物体

如果需要在线程之间共享状态,则状态应该是不可变的。 如果一个线程需要对对象进行更改,它会使用更改创建对象的全新版本,而不是改变对象的状态。

不可变性本身并不限制您可以编写的代码类型,也不是低效的。 有许多不可变堆栈的实现,构成映射和集合基础的各种不可变树,以及其他类型的不可变数据结构,并且许多(如果不是全部)不可变数据结构与它们的可变对应物一样有效。

由于对象是不可变的,因此一个线程不可能在您的鼻子下改变共享状态。 这意味着您不需要获取锁来编写multithreading代码。 这种方法消除了与死锁,活锁和竞争条件相关的一整类错误。

Erlang风格的消息传递

您不需要学习该语言,但请查看Erlang以了解它如何实现并发。 Erlang应用程序可以无限扩展,因为每个进程都与其他进程完全分离(注意:这些不完全是进程,但也不完全是线程)。

进程启动并简单地旋转循环等待消息:消息以元组的forms接收,然后进程可以模式匹配以查看消息是否有意义。 进程可以发送其他消息,但它们对收到消息的人无动于衷。

这种风格的优点是消除锁定,当一个进程失败时它不会导致整个应用程序崩溃。 以下是Erlang风格并发的一个很好的总结: http : //www.defmacro.org/ramblings/concurrency.html

使用FIFO。 其中很多。 这是硬件程序员的古老秘密,它不止一次地保存了我的培根。

这些是编写质量(更易于阅读和理解)multithreading代码的步骤:

  1. 查看Jeffrey Richter的Power Threading Library
  2. 观看video – 惊讶
  3. 花些时间深入了解实际情况,请阅读“ 此处发现的同时事件”文章。
  4. 开始编写健壮,安全的multithreading应用程序!
  5. 意识到它仍然不是那么简单,犯了一些错误并从中学习……重复……重复……重复:-)

似乎没有人回答如何调试multithreading程序的问题。 这是一个真正的挑战,因为如果存在错误,则需要实时调查,这对于Visual Studio等大多数工具来说几乎是不可能的。 唯一可行的解​​决方案是编写跟踪,尽管跟踪本身应该:

  1. 不添加任何延迟
  2. 不使用任何锁定
  3. multithreading安全
  4. 跟踪正确顺序发生的事情。

这听起来像是一项不可能完成的任务,但通过将跟踪写入内存可以轻松实现。 在C#中,它看起来像这样:

public const int MaxMessages = 0x100; string[] messages = new string[MaxMessages]; int messagesIndex = -1; public void Trace(string message) { int thisIndex = Interlocked.Increment(ref messagesIndex); messages[thisIndex] = message; } 

Trace()方法是multithreading安全的,非阻塞的,可以从任何线程调用。 在我的电脑上,执行大约需要2微秒,这应该足够快。

在您认为可能出错的地方添加Trace()指令,让程序运行,等到错误发生,停止跟踪然后调查跟踪是否有任何错误。

这种方法的更详细的描述,它还收集线程和时间信息,循环缓冲区并输出跟踪,你可以找到:CodeProject:实时调试multithreading代码1