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

时间:2009-01-29 20:39:58

标签: c# .net vb.net multithreading

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

除了上面提到的并发修改问题外,还有一些可能遇到的死锁,竞争条件等问题。 由于调试/修复线程问题很痛苦,我想知道如何减少这方面的编码错误/错误以及如何更容易找到它们。那么,我要求的是什么,是:

  • 编写多线程代码时是否有任何好的模式?什么是Dos和Don'ts?
  • 您使用哪些技巧来调试线程问题?

如果适用且可能,请提供一些示例代码。答案应该与.NET框架(任何版本)相关。

5 个答案:

答案 0 :(得分:25)

这可能是一个大量的列表 - 阅读Joe Duffy的优秀“Concurrent Programming On 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来提高看到竞争条件和死锁的可能性是可行的,但是在你知道把它放到哪里之前你必须对错误有一个合理的理解。记录非常方便,但不要忘记代码进入某种量子状态 - 通过记录来观察它几乎必然会改变它的行为!

答案 1 :(得分:11)

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

不可变对象

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

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

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

Erlang风格的消息传递

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

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

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

答案 2 :(得分:2)

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

答案 3 :(得分:2)

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

  1. 不添加任何延迟
  2. 不使用任何锁定
  3. 多线程安全
  4. 以正确的顺序跟踪发生的事情。
  5. 这听起来像是一项不可能完成的任务,但可以通过将跟踪写入内存来轻松实现。在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()是多线程安全的,非阻塞的,可以从任何线程调用。在我的电脑上,执行大约需要2微秒,这应该足够快。

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

    此方法的更详细说明,它还收集线程和计时信息,循环缓冲区并输出跟踪,您可以在以下位置找到: CodeProject:实时调试多线程代码1

答案 4 :(得分:0)

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

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