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

时间:2022-06-19 20:58:59

Today I had to fix some older VB.NET 1.0 code which is using threads. The problem was with updating UI elements from the worker thread instead of the UI-thread. It took me some time to find out that I can use assertions with InvokeRequired to find the problem.

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

Besides the above mentioned concurrent modification problem, there are deadlocks, race conditions, etc. one could run into. As debugging/fixing threading problems is a pain, I'm wondering how I could reduce coding errors/faults in this area and how I could easier find any of them. So, what I'm asking for, is:

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

  • Are there any good patterns to follow when writing multi-threading code? What are the Dos and Don'ts?
  • 编写多线程代码时是否有任何好的模式?什么是Dos和Don'ts?
  • What techniques do you use to debug threading problems?
  • 您使用什么技术来调试线程问题?

Please provide some example code if applicable and possible. The answers should be related to the .NET framework (any version).

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

5 个解决方案

#1


24  

This could be a massive list - read Joe Duffy's excellent "Concurrent Programming On Windows" for much more detail. This is pretty much a brain dump...

这可能是一个庞大的列表 - 阅读Joe Duffy的优秀“Windows上的并发编程”以获取更多细节。这几乎是一个大脑转储......

  • Try to avoid calling into significant chunks of code while you own a lock
  • 在拥有锁时,尽量避免调用大量代码
  • Avoid locking on references which code outside the class might also lock on
  • 避免锁定类外部代码也可能锁定的引用
  • If you ever need to acquire more than one lock at a time, always acquire those locks in the same order
  • 如果您需要一次购买多个锁,请始终以相同的顺序获取这些锁
  • Where reasonable, use immutable types - they can be shared freely between threads
  • 在合理的情况下,使用不可变类型 - 它们可以在线程之间*共享
  • Other than immutable types, try to avoid the need to share data between threads
  • 除了不可变类型之外,尽量避免在线程之间共享数据
  • Avoid trying to make your types threadsafe; most types don't need to be, and usually the code which needs to share data will need to control the locking itself
  • 避免尝试使你的类型线程安全;大多数类型不需要,通常需要共享数据的代码需要控制锁定本身
  • In a WinForms app:
    • Don't perform any long-running or blocking operations on the UI thread
    • 不要在UI线程上执行任何长时间运行或阻塞操作
    • Don't touch the UI from any thread other than the UI thread. (Use BackgroundWorker, Control.Invoke/BeginInvoke)
    • 不要从UI线程以外的任何线程触摸UI。 (使用BackgroundWorker,Control.Invoke / BeginInvoke)
  • 在WinForms应用程序中:不要在UI线程上执行任何长时间运行或阻塞操作不要从UI线程以外的任何线程触摸UI。 (使用BackgroundWorker,Control.Invoke / BeginInvoke)
  • Avoid thread-local variables (aka thread-statics) where possible - they can lead to unexpected behaviour, particularly on ASP.NET where a request may be served by different threads (search for "thread agility" and ASP.NET)
  • 尽可能避免线程局部变量(也就是线程静态) - 它们可能导致意外行为,尤其是在ASP.NET上,其中请求可能由不同的线程提供服务(搜索“线程敏捷性”和ASP.NET)
  • Don't try to be clever. Lock-free concurrent code is hugely difficult to get right.
  • 不要试图聪明。无锁定并发代码很难做到正确。
  • Document the threading model (and thread safety) of your types
  • 记录类型的线程模型(和线程安全性)
  • Monitor.Wait should almost always be used in conjunction with some sort of check, in a while loop (i.e. while (I can't proceed) Monitor.Wait(monitor))
  • Monitor.Wait应该几乎总是与某种检查一起使用,在while循环中(即while(我无法继续)Monitor.Wait(monitor))
  • Consider the difference between Monitor.Pulse and Monitor.PulseAll carefully every time you use one of them.
  • 每次使用其中一个时,请仔细考虑Monitor.Pulse和Monitor.PulseAll之间的区别。
  • Inserting Thread.Sleep to make a problem go away is never a real fix.
  • 插入Thread.Sleep以使问题消失绝不是真正的解决方法。
  • Have a look at "Parallel Extensions" and the "Coordination and Concurrency Runtime" as ways of making concurrency simpler. Parallel Extensions is going to be part of .NET 4.0.
  • 看看“并行扩展”和“协调和并发运行时”作为使并发更简单的方法。 Parallel Extensions将成为.NET 4.0的一部分。

In terms of debugging, I don't have very much advice. Using Thread.Sleep to boost the chances of seeing race conditions and deadlocks can work, but you've got to have quite a reasonable understanding of what's wrong before you know where to put it. Logging is very handy, but don't forget that the code goes into a sort of quantum state - observing it via logging is almost bound to change its behaviour!

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

#2


11  

I'm not sure how well this will help for the particular application you're working with, but here are two approaches borrowed from functional programming for writing multithreaded code:

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

Immutable objects

不可变的物体

If you need to share state between threads, the state should be immutable. If one thread needs to make a change to the object, it creates a brand new version of the object with the change instead of mutating the object's state.

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

Immutability does not inherently limit the kind of code you can write, nor is it inefficient. There are lots of implementations of immutable stacks, a variety of immutable trees that form the basis of maps and sets, and other kinds of immutable data structures, and many (if not all) immutable data structures are just as efficient as their mutable counterparts.

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

Since objects are immutable, its not possible for one thread to mutate shared state under your nose. This means you don't need to acquire locks to write multithreaded code. This approach eliminates a whole class of errors related to deadlocking, livelocking, and raceconditions.

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

Erlang-style message passing

Erlang风格的消息传递

You don't need to learn the language, but have a look at Erlang to see how it approaches concurrency. Erlang apps can scale pretty much indefinitely because each process is completely seperate from all the others (note: these are not exactly processes, but not exactly threads either).

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

Processes fire up and simply spin a loop waiting for messages: messages are recieved in the form of tuples, which the process can then pattern match against to see if the message is meaningful. Processes can send other messages, but they are indifferent to whoever recieves the message.

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

Advantanges to this style is an elimination of locks, when one process fails it doesn't bring down your entire app. Here's a nice summary of Erlang-style concurrency: http://www.defmacro.org/ramblings/concurrency.html

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

#3


2  

Use FIFOs. Lots of them. It's the ancient secret of the hardware programmer, and it's saved my bacon more than once.

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

#4


1  

These are the steps to writing quality (easier to read and understand) multi threaded code:

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

  1. Check out the Power Threading Library by Jeffrey Richter
  2. 查看Jeffrey Richter的Power Threading Library
  3. Watch the video - be amazed
  4. 观看视频 - 惊讶
  5. Take some time to gain a more in depth understanding of what's really going on, read the 'Concurrent Affair's articles found here.
  6. 花些时间深入了解实际情况,请阅读“此处发现的同时事件”文章。
  7. Start writing robust, safe multi threaded apps!
  8. 开始编写健壮,安全的多线程应用程序!
  9. Realise it's still not that simple, make some mistakes and learn from them ...repeat...repeat...repeat :-)
  10. 意识到它仍然不是那么简单,犯了一些错误并从中学习......重复......重复......重复:-)

#5


1  

It seems nobody answered the question how to debug multithreaded programs. This is a real challenge, because if there is a bug,it needs to be investigated in real time, which is nearly impossible with most tools like Visual Studio. The only practical solution is to write traces, although the tracing itself should:

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

  1. not add any delay
  2. 不添加任何延迟
  3. not use any locking
  4. 不使用任何锁定
  5. be multithreading safe
  6. 多线程安全
  7. trace what happened in the correct sequence.
  8. 跟踪正确顺序发生的事情。

This sounds like an impossible task, but it can be easily achieved by writing the trace into memory. In C#, it would look something like this:

这听起来像是一项不可能完成的任务,但通过将跟踪写入内存可以轻松实现。在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;
}

The method Trace() is multithreading safe, non blocking and can be called from any thread. On my PC, it takes about 2 microseconds to execute, which should be fast enough.

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

Add Trace() instructions wherever you think something might go wrong, let the program run, wait until the error happens, stop the trace and then investigate the trace for any errors.

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

A more detailed description for this approach which also collects thread and timing information, recycles the buffer and outputs the trace nicely you can find at: CodeProject: Debugging multithreaded code in real time 1

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

#1


24  

This could be a massive list - read Joe Duffy's excellent "Concurrent Programming On Windows" for much more detail. This is pretty much a brain dump...

这可能是一个庞大的列表 - 阅读Joe Duffy的优秀“Windows上的并发编程”以获取更多细节。这几乎是一个大脑转储......

  • Try to avoid calling into significant chunks of code while you own a lock
  • 在拥有锁时,尽量避免调用大量代码
  • Avoid locking on references which code outside the class might also lock on
  • 避免锁定类外部代码也可能锁定的引用
  • If you ever need to acquire more than one lock at a time, always acquire those locks in the same order
  • 如果您需要一次购买多个锁,请始终以相同的顺序获取这些锁
  • Where reasonable, use immutable types - they can be shared freely between threads
  • 在合理的情况下,使用不可变类型 - 它们可以在线程之间*共享
  • Other than immutable types, try to avoid the need to share data between threads
  • 除了不可变类型之外,尽量避免在线程之间共享数据
  • Avoid trying to make your types threadsafe; most types don't need to be, and usually the code which needs to share data will need to control the locking itself
  • 避免尝试使你的类型线程安全;大多数类型不需要,通常需要共享数据的代码需要控制锁定本身
  • In a WinForms app:
    • Don't perform any long-running or blocking operations on the UI thread
    • 不要在UI线程上执行任何长时间运行或阻塞操作
    • Don't touch the UI from any thread other than the UI thread. (Use BackgroundWorker, Control.Invoke/BeginInvoke)
    • 不要从UI线程以外的任何线程触摸UI。 (使用BackgroundWorker,Control.Invoke / BeginInvoke)
  • 在WinForms应用程序中:不要在UI线程上执行任何长时间运行或阻塞操作不要从UI线程以外的任何线程触摸UI。 (使用BackgroundWorker,Control.Invoke / BeginInvoke)
  • Avoid thread-local variables (aka thread-statics) where possible - they can lead to unexpected behaviour, particularly on ASP.NET where a request may be served by different threads (search for "thread agility" and ASP.NET)
  • 尽可能避免线程局部变量(也就是线程静态) - 它们可能导致意外行为,尤其是在ASP.NET上,其中请求可能由不同的线程提供服务(搜索“线程敏捷性”和ASP.NET)
  • Don't try to be clever. Lock-free concurrent code is hugely difficult to get right.
  • 不要试图聪明。无锁定并发代码很难做到正确。
  • Document the threading model (and thread safety) of your types
  • 记录类型的线程模型(和线程安全性)
  • Monitor.Wait should almost always be used in conjunction with some sort of check, in a while loop (i.e. while (I can't proceed) Monitor.Wait(monitor))
  • Monitor.Wait应该几乎总是与某种检查一起使用,在while循环中(即while(我无法继续)Monitor.Wait(monitor))
  • Consider the difference between Monitor.Pulse and Monitor.PulseAll carefully every time you use one of them.
  • 每次使用其中一个时,请仔细考虑Monitor.Pulse和Monitor.PulseAll之间的区别。
  • Inserting Thread.Sleep to make a problem go away is never a real fix.
  • 插入Thread.Sleep以使问题消失绝不是真正的解决方法。
  • Have a look at "Parallel Extensions" and the "Coordination and Concurrency Runtime" as ways of making concurrency simpler. Parallel Extensions is going to be part of .NET 4.0.
  • 看看“并行扩展”和“协调和并发运行时”作为使并发更简单的方法。 Parallel Extensions将成为.NET 4.0的一部分。

In terms of debugging, I don't have very much advice. Using Thread.Sleep to boost the chances of seeing race conditions and deadlocks can work, but you've got to have quite a reasonable understanding of what's wrong before you know where to put it. Logging is very handy, but don't forget that the code goes into a sort of quantum state - observing it via logging is almost bound to change its behaviour!

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

#2


11  

I'm not sure how well this will help for the particular application you're working with, but here are two approaches borrowed from functional programming for writing multithreaded code:

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

Immutable objects

不可变的物体

If you need to share state between threads, the state should be immutable. If one thread needs to make a change to the object, it creates a brand new version of the object with the change instead of mutating the object's state.

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

Immutability does not inherently limit the kind of code you can write, nor is it inefficient. There are lots of implementations of immutable stacks, a variety of immutable trees that form the basis of maps and sets, and other kinds of immutable data structures, and many (if not all) immutable data structures are just as efficient as their mutable counterparts.

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

Since objects are immutable, its not possible for one thread to mutate shared state under your nose. This means you don't need to acquire locks to write multithreaded code. This approach eliminates a whole class of errors related to deadlocking, livelocking, and raceconditions.

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

Erlang-style message passing

Erlang风格的消息传递

You don't need to learn the language, but have a look at Erlang to see how it approaches concurrency. Erlang apps can scale pretty much indefinitely because each process is completely seperate from all the others (note: these are not exactly processes, but not exactly threads either).

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

Processes fire up and simply spin a loop waiting for messages: messages are recieved in the form of tuples, which the process can then pattern match against to see if the message is meaningful. Processes can send other messages, but they are indifferent to whoever recieves the message.

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

Advantanges to this style is an elimination of locks, when one process fails it doesn't bring down your entire app. Here's a nice summary of Erlang-style concurrency: http://www.defmacro.org/ramblings/concurrency.html

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

#3


2  

Use FIFOs. Lots of them. It's the ancient secret of the hardware programmer, and it's saved my bacon more than once.

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

#4


1  

These are the steps to writing quality (easier to read and understand) multi threaded code:

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

  1. Check out the Power Threading Library by Jeffrey Richter
  2. 查看Jeffrey Richter的Power Threading Library
  3. Watch the video - be amazed
  4. 观看视频 - 惊讶
  5. Take some time to gain a more in depth understanding of what's really going on, read the 'Concurrent Affair's articles found here.
  6. 花些时间深入了解实际情况,请阅读“此处发现的同时事件”文章。
  7. Start writing robust, safe multi threaded apps!
  8. 开始编写健壮,安全的多线程应用程序!
  9. Realise it's still not that simple, make some mistakes and learn from them ...repeat...repeat...repeat :-)
  10. 意识到它仍然不是那么简单,犯了一些错误并从中学习......重复......重复......重复:-)

#5


1  

It seems nobody answered the question how to debug multithreaded programs. This is a real challenge, because if there is a bug,it needs to be investigated in real time, which is nearly impossible with most tools like Visual Studio. The only practical solution is to write traces, although the tracing itself should:

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

  1. not add any delay
  2. 不添加任何延迟
  3. not use any locking
  4. 不使用任何锁定
  5. be multithreading safe
  6. 多线程安全
  7. trace what happened in the correct sequence.
  8. 跟踪正确顺序发生的事情。

This sounds like an impossible task, but it can be easily achieved by writing the trace into memory. In C#, it would look something like this:

这听起来像是一项不可能完成的任务,但通过将跟踪写入内存可以轻松实现。在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;
}

The method Trace() is multithreading safe, non blocking and can be called from any thread. On my PC, it takes about 2 microseconds to execute, which should be fast enough.

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

Add Trace() instructions wherever you think something might go wrong, let the program run, wait until the error happens, stop the trace and then investigate the trace for any errors.

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

A more detailed description for this approach which also collects thread and timing information, recycles the buffer and outputs the trace nicely you can find at: CodeProject: Debugging multithreaded code in real time 1

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