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

21

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

除了上述的并发修改问题,还有死锁、竞态条件等其他问题,人们可能会遇到。因为调试/修复线程问题很麻烦,所以我想知道如何减少这个区域的编码错误/故障,并且如何更容易地找到它们。因此,我的问题是:

  • 在编写多线程代码时是否有任何好的模式可供遵循?有哪些应该做和不应该做的事情?
  • 您用什么技术来调试线程问题?

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

5个回答

25

这可能是一个庞大的列表 - 阅读乔·达菲出色的 "Windows上的并发编程" 以获取更多细节。这基本上是一个头脑倾泻...

  • 尽量避免在拥有锁的情况下调用大块代码
  • 避免在引用上锁定,外部代码也可能会锁定它们
  • 如果需要同时获取多个锁,请始终按相同顺序获取这些锁
  • 在合理的情况下,使用不可变类型 - 它们可以在线程之间自由共享
  • 除了不可变类型,尽量避免在线程之间共享数据的需求
  • 避免尝试使您的类型线程安全;大多数类型不需要,并且通常需要共享数据的代码将需要控制锁定本身
  • 在WinForms应用程序中:
    • 不要在UI线程上执行任何长时间运行或阻塞操作
    • 不要从除UI线程以外的任何线程触摸UI。(使用BackgroundWorker、Control.Invoke/BeginInvoke)
  • 尽可能避免使用线程本地变量(也称为线程静态变量)- 它们可能会导致意外行为,特别是在ASP.NET上,其中请求可能由不同的线程服务(搜索“线程敏捷性”和ASP.NET)
  • 不要试图聪明。无锁并发代码非常难以正确实现。
  • 记录类型的线程模型(和线程安全性)
  • Monitor.Wait几乎总是与某种检查一起在while循环中使用(即 while (I can't proceed) Monitor.Wait(monitor))
  • 每次使用Monitor.Pulse和Monitor.PulseAll时,请仔细考虑它们之间的区别。
  • 插入Thread.Sleep以解决问题永远不是真正的解决方法。
  • 请查看“Parallel Extensions”和“Coordination and Concurrency Runtime”,以使并发性更简单。Parallel Extensions将成为.NET 4.0的一部分。

关于调试,我没有太多的建议。使用Thread.Sleep来增加看到竞态条件和死锁的机会可能有效,但是在知道放置位置之前,您必须对问题有相当合理的了解。日志记录非常方便,但不要忘记代码会进入一种量子状态-通过日志记录观察它几乎肯定会改变其行为!


3
“但不要忘记代码进入了某种量子状态” - 叹气 我太了解那个问题了。 - Rob

11

我不确定这对你正在处理的特定应用程序有多大帮助,但以下是两种函数式编程借鉴的方法,可用于编写多线程代码:

不可变对象

如果需要在线程之间共享状态,则状态应该是不可变的。如果一个线程需要更改对象的状态,则应创建一个具有更改内容的全新版本,而不是改变对象的状态。

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

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

Erlang风格的消息传递

不需要学习这种语言,但可以看看Erlang如何处理并发。Erlang应用程序可以快速扩展,因为每个进程都完全独立于所有其他进程(note:这些不完全是进程,但也不完全是线程)。

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

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


1
不错的答案。很高兴看到一些关注问题的高级方法,而不仅仅是列出所有特定于.NET的同步原语。 :) - jalf
链接已经失效了,你有 http://www.defmacro.org/ramblings/concurrency.html 的新链接吗? - Sundar Rajan
我认为它已被删除。这是wayback machine链接 - Default

2

使用FIFO。使用大量的FIFO。这是硬件程序员的古老秘密,它已经多次拯救了我的麻烦。


2
似乎没有人回答如何调试多线程程序的问题。这是一个真正的挑战,因为如果有错误,它需要在实时中进行调查,但使用像Visual Studio这样的大多数工具几乎是不可能的。唯一的实际解决方案是编写跟踪代码,虽然跟踪本身应该:
  1. 不添加任何延迟
  2. 不使用任何锁定
  3. 支持多线程
  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()是多线程安全的,非阻塞的,并且可以从任何线程调用。在我的电脑上,它执行大约需要2微秒,这应该足够快了。
在您认为可能出现问题的地方添加Trace()指令,让程序运行,等待错误发生,停止跟踪,然后调查跟踪是否有错误。
关于这种方法的更详细描述,还包括收集线程和时间信息、回收缓冲区并漂亮地输出跟踪结果,您可以在CodeProject找到: 实时调试多线程代码 1

0

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

  1. 查看 Jeffrey Richter 的 Power Threading Library
  2. 观看视频-感到惊讶。
  3. 花些时间更深入地了解正在发生的事情,阅读此处Concurrent Affair's articles
  4. 开始编写强大、安全的多线程应用程序!
  5. 意识到这仍然不那么简单,犯一些错误并从中学习...重复...重复...重复 :-)

链接已经失效,此回答现在没有任何实际意义。 - Sundar Rajan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接