2009-01-29 44 views
18

今天我不得不修复一些使用线程的较老的VB.NET 1.0代码。问题是从工作线程而不是UI线程更新UI元素。我花了一些时间才发现可以使用InvokeRequired的断言来查找问题。如何在.NET中编写安全/正确的多线程代码?

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

  • 有什么好看的图案编写多线程代码时要遵循?什么是Dos和不该做的事?
  • 你用什么技术来调试线程问题?

请在适用和可能的情况下提供一些示例代码。答案应该与.NET框架(任何版本)相关。

回答

24

这可能是一个大规模的列表 - 阅读Joe Duffy的优秀“Concurrent Programming On Windows”了解更多细节。这是一个很值得倾吐心事......

  • 尽量避免调入的代码显著块,而你自己的锁
  • 避免锁死在引用该代码的类外还可以锁定
  • 如果您需要一次获得多个锁,总是以相同的顺序获取这些锁
  • 在合理的情况下,使用不可变类型 - 它们可以在线程之间自由共享
  • 除了不可变类型之外,尝试避免需要在线程之间共享数据
  • 避免尝试使您的类型线程安全;大多数类型不必是,通常这需要共享数据的代码将需要控制锁定本身
  • 在WinForms应用程序:
    • 不要执行任何长时间运行或阻塞操作UI线程
    • 请勿触摸UI线程以外的任何线程的UI。 (使用BackgroundWorker,Control.Invoke/BeginInvoke)
  • 尽可能避免线程局部变量(又名线程静态) - 它们可能会导致意外的行为,特别是在ASP.NET中,请求可能由不同服务线程(搜索“线程敏捷性”和ASP.NET)
  • 不要试图变得聪明。无锁并发码巨大很难找到正确的。
  • 记录您的类型的线程模型(和线程安全性)
  • Monitor.Wait应该几乎总是与某种检查结合使用,在while循环中(即while(I can not proceed)Monitor)。等待(监视器))
  • 每次使用Monitor.Pulse和Monitor.PulseAll时都要仔细考虑一下。
  • 插入Thread.Sleep使问题消失永远不是一个真正的解决方案。
  • 查看“并行扩展”和“协调和并发运行时”是使并发更简单的方法。并行扩展将成为.NET 4.0的一部分。

在调试方面,我没有太多的建议。使用Thread.Sleep来增加看到竞争条件和僵局的机会是可行的,但在你知道该把它放到哪里之前,你必须对错误有一个合理的理解。记录非常方便,但不要忘记代码会进入某种量子状态 - 通过记录观察它几乎肯定会改变它的行为!

+3

“但不忘记代码进入某种量子状态“ - *叹*我很清楚这个问题。 – 2009-01-29 21:02:27

11

我不知道有多好,这将有助于你正在使用的特定应用程序,但这里有两种方法从函数式编程借用编写多线程代码:

不可变对象

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

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

因为对象是不可变的,所以它不可能让一个线程在你的鼻子下改变共享状态。这意味着你不需要获取锁来编写多线程代码。这种方法消除了与死锁,活锁和竞赛有关的一整类错误。

二郎式消息传递

你不需要学习语言,但看看二郎,看看它如何处理并发。 Erlang应用程序可以无限期地扩展,因为每个进程完全独立于其他所有进程(注意:这些并不完全是进程,但也不完全是线程)。

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

这种风格的优势在于消除了锁,当一个进程失败时,它不会降低您的整个应用程序。这里的二郎式并发的一个很好的总结:http://www.defmacro.org/ramblings/concurrency.html

+1

很好的答案。很高兴看到一些关注高级方法的问题,而不是仅列出所有.NET特定的同步原语。 :) – jalf 2009-01-31 22:12:55

1

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

  1. 退房的Power Threading Library通过杰弗里里希特
  2. 观看视频 - 惊讶
  3. 花一些时间来深入了解真正发生的事情,阅读“并发事件”的文章找到here
  4. 开始编写健壮,安全的多线程应用程序!
  5. 实现它仍然不是那么简单,犯一些错误,并从中学习... ...重复重复...重复:-)
2

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

1

似乎没人回答如何调试多线程程序的问题。这是一个真正的挑战,因为如果存在错误,就需要实时进行调查,而使用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; 
} 

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

在您认为出现问题的地方添加Trace()指令,让程序运行,等到错误发生时停止跟踪,然后调查跟踪以查找任何错误。

用于这种方法也收集线程和定时信息更详细的说明,回收缓冲输出跟踪很好,你可以找到在: CodeProject上:实时调试多线程代码1