简体   繁体   English

C#中的线程安全异步代码

[英]Thread-safe asynchronous code in C#

I asked the question below couple of weeks ago. 几个星期前我问了下面的问题。 Now, when reviewing my question and all the answers, a very important detail jumped into my eyes: In my second code example, isn't DoTheCodeThatNeedsToRunAsynchronously() executed in the main (UI) thread? 现在,在回顾我的问题和所有答案时,一个非常重要的细节跳进了我的眼睛:在我的第二个代码示例中,是不是在主(UI)线程中执行的DoTheCodeThatNeedsToRunAsynchronously() Doesn't the timer just wait a second and then post an event to the main thread? 计时器只是等待一秒钟,然后将事件发布到主线程? This would mean then that the code-that-needs-to-run-asynchronously isn't run asynchronously at all?! 这意味着那些需要异步运行的代码根本不是异步运行的?!

Original question: 原始问题:


I have recently faced a problem multiple times and solved it in different ways, always being uncertain on whether it is thread safe or not: I need to execute a piece of C# code asynchronously. 我最近遇到过多次问题并以不同的方式解决它,总是不确定它是否是线程安全的:我需要异步执行一段C#代码。 ( Edit: I forgot to mention I'm using .NET 3.5! ) 编辑:我忘了提到我使用的是.NET 3.5!

That piece of code works on an object that is provided by the main thread code. 这段代码适用于主线程代码提供的对象。 (Edit: Let's assume that object is thread-safe in itself.) I'll present you two ways I tried (simplified) and have these four questions : (编辑:我们假设该对象本身就是线程安全的。)我将向您介绍我尝试过的两种方法(简化)并提出以下四个问题

  1. What is the best way to achieve what I want? 实现我想要的最好方法是什么? Is it one of the two or another approach? 它是两种或另一种方法之一吗?
  2. Is one of the two ways not thread-safe (I fear both...) and why? 两种方式中的一种不是线程安全的(我担心两者......)以及为什么?
  3. The first approach creates a thread and passes it the object in the constructor. 第一种方法创建一个线程并将其传递给构造函数中的对象。 Is that how I'm supposed to pass the object? 这是我应该如何传递对象?
  4. The second approach uses a timer which doesn't provide that possibility, so I just use the local variable in the anonymous delegate. 第二种方法使用不提供这种可能性的计时器,所以我只使用匿名委托中的局部变量。 Is that safe or is it possible in theory that the reference in the variable changes before it is evaluated by the delegate code? 这是安全还是理论上可以在变量中的引用在委托代码评估之前发生变化? ( This is a very generic question whenever one uses anonymous delegates ). 每当使用匿名代表时,这是一个非常通用的问题 )。 In Java you are forced to declare the local variable as final (ie it cannot be changed once assigned). 在Java中,您必须将局部变量声明为final (即一旦分配就不能更改)。 In C# there is no such possibility, is there? 在C#中没有这种可能性,有吗?

Approach 1: Thread 方法1:线程

new Thread(new ParameterizedThreadStart(
    delegate(object parameter)
    {
        Thread.Sleep(1000); // wait a second (for a specific reason)

        MyObject myObject = (MyObject)parameter;

        DoTheCodeThatNeedsToRunAsynchronously();
        myObject.ChangeSomeProperty();

    })).Start(this.MyObject);

There is one problem I had with this approach: My main thread might crash, but the process still persists in the memory due to the zombie thread. 这种方法有一个问题:我的主线程可能崩溃,但由于僵尸线程,进程仍然存在于内存中。


Approach 2: Timer 方法2:定时器

MyObject myObject = this.MyObject;

System.Timers.Timer timer = new System.Timers.Timer();
timer.Interval = 1000;
timer.AutoReset = false; // i.e. only run the timer once.
timer.Elapsed += new System.Timers.ElapsedEventHandler(
    delegate(object sender, System.Timers.ElapsedEventArgs e)
    {
        DoTheCodeThatNeedsToRunAsynchronously();
        myObject.ChangeSomeProperty();
    });

DoSomeStuff();
myObject = that.MyObject; // hypothetical second assignment.

The local variable myObject is what I'm talking about in question 4. I've added a second assignment as an example. 局部变量myObject就是我在问题4中所讨论的内容。我添加了第二个赋值作为示例。 Imagine the timer elapses after the second assigment, will the delegate code operate on this.MyObject or that.MyObject ? 想象一下,在第二个分配之后计时器this.MyObject ,委托代码是否会对this.MyObjectthat.MyObject

Whether or not either of these pieces of code is safe has to do with the structure of MyObject instances. 这些代码中的任何一个是否安全都与MyObject实例的结构有关。 In both cases you are sharing the myObject variable between the foreground and background threads. 在这两种情况下,您都在前台线程和后台线程之间共享myObject变量。 There is nothing stopping the foreground thread from modifying myObject while the background thread is running. 在后台线程运行时,没有什么能阻止前台线程修改myObject

This may or may not be safe and depends on the structure of MyObject . 这可能是也可能不安全,取决于MyObject的结构。 However if you haven't specifically planned for it then it's most certainly an unsafe operation. 但是,如果您没有专门为此计划,那么它肯定是一种不安全的操作。

I recommend using Task objects, and restructuring the code so that the background task returns its calculated value rather than changing some shared state. 我建议使用Task对象,并重构代码,以便后台任务返回其计算值而不是更改某些共享状态。

I have a blog entry that discusses five different approaches to background tasks ( Task , BackgroundWorker , Delegate.BeginInvoke , ThreadPool.QueueUserWorkItem , and Thread ), with the pros and cons of each. 我有一个博客条目 ,讨论了五种不同的后台任务方法( TaskBackgroundWorkerDelegate.BeginInvokeThreadPool.QueueUserWorkItemThread ),各有利弊。

To answer your questions specifically: 要具体回答你的问题:

  1. What is the best way to achieve what I want? 实现我想要的最好方法是什么? Is it one of the two or another approach? 它是两种或另一种方法之一吗? The best solution is to use the Task object instead of a specific Thread or timer callback. 最好的解决方案是使用Task对象而不是特定的Thread或计时器回调。 See my blog post for all the reasons why, but in summary: Task supports returning a result , callbacks on completion , proper error handling , and integration with the universal cancellation system in .NET. 请参阅我的博客文章,了解其中的所有原因,但总结如下: Task支持返回结果完成时回调正确的错误处理以及与.NET中的通用取消系统的集成。
  2. Is one of the two ways not thread-safe (I fear both...) and why? 两种方式中的一种不是线程安全的(我担心两者......)以及为什么? As others have stated, this totally depends on whether MyObject.ChangeSomeProperty is threadsafe. 正如其他人所说,这完全取决于MyObject.ChangeSomeProperty是否是线程安全的。 When dealing with asynchronous systems, it's easier to reason about threadsafety when each asynchronous operation does not change shared state, and rather returns a result . 当与异步系统处理,它更容易推理threadsafety当每个异步操作不会更改共享状态,而返回的结果
  3. The first approach creates a thread and passes it the object in the constructor. 第一种方法创建一个线程并将其传递给构造函数中的对象。 Is that how I'm supposed to pass the object? 这是我应该如何传递对象? Personally, I prefer using lambda binding, which is more type-safe (no casting necessary). 就个人而言,我更喜欢使用lambda绑定,这更加类型安全(不需要强制转换)。
  4. The second approach uses a timer which doesn't provide that possibility, so I just use the local variable in the anonymous delegate. 第二种方法使用不提供这种可能性的计时器,所以我只使用匿名委托中的局部变量。 Is that safe or is it possible in theory that the reference in the variable changes before it is evaluated by the delegate code? 这是安全还是理论上可以在变量中的引用在委托代码评估之前发生变化? Lambdas (and delegate expressions) bind to variables , not to values , so the answer is yes: the reference may change before it is used by the delegate. Lambdas(和委托表达式)绑定到变量 ,而不是 ,因此答案是肯定的:引用可能会在委托使用之前更改。 If the reference may change, then the usual solution is to create a separate local variable that is only used by the lambda expression, 如果引用可能会改变,那么通常的解决方案是创建一个仅由lambda表达式使用的单独局部变量,

as such: 因此:

MyObject myObject = this.MyObject;
...
timer.AutoReset = false; // i.e. only run the timer once.
var localMyObject = myObject; // copy for lambda
timer.Elapsed += new System.Timers.ElapsedEventHandler(
  delegate(object sender, System.Timers.ElapsedEventArgs e)
  {
    DoTheCodeThatNeedsToRunAsynchronously();
    localMyObject.ChangeSomeProperty();
  });
// Now myObject can change without affecting timer.Elapsed

Tools like ReSharper will try to detect whether local variables bound in lambdas may change, and will warn you if it detects this situation. 像ReSharper这样的工具会尝试检测lambdas中绑定的局部变量是否会发生变化,如果检测到这种情况,它会发出警告。

My recommended solution (using Task ) would look something like this: 我推荐的解决方案(使用Task )看起来像这样:

var ui = TaskScheduler.FromCurrentSynchronizationContext();
var localMyObject = this.myObject;
Task.Factory.StartNew(() =>
{
  // Run asynchronously on a ThreadPool thread.
  Thread.Sleep(1000); // TODO: review if you *really* need this   

  return DoTheCodeThatNeedsToRunAsynchronously();   
}).ContinueWith(task =>
{
  // Run on the UI thread when the ThreadPool thread returns a result.
  if (task.IsFaulted)
  {
    // Do some error handling with task.Exception
  }
  else
  {
    localMyObject.ChangeSomeProperty(task.Result);
  }
}, ui);

Note that since the UI thread is the one calling MyObject.ChangeSomeProperty , that method doesn't have to be threadsafe. 请注意,由于UI线程是调用MyObject.ChangeSomeProperty线程,因此该方法不必是线程安全的。 Of course, DoTheCodeThatNeedsToRunAsynchronously still does need to be threadsafe. 当然, DoTheCodeThatNeedsToRunAsynchronously仍然需要线程安全。

Your first attempt is pretty good, but the thread continued to exist even after the application exits, because you didn't set the IsBackground property to true ... here is a simplified (and improved) version of your code: 你的第一次尝试非常好,但是即使在应用程序退出之后线程仍然存在,因为你没有将IsBackground属性设置为true ...这里是你的代码的简化(和改进)版本:

MyObject myObject = this.MyObject;
Thread t = new Thread(()=>
    {
        Thread.Sleep(1000); // wait a second (for a specific reason)
        DoTheCodeThatNeedsToRunAsynchronously();
        myObject.ChangeSomeProperty();
    });
t.IsBackground = true;
t.Start();

With regards to the thread safety: it's difficult to tell if your program functions correctly when multiple threads execute simultaneously, because you're not showing us any points of contention in your example. 关于线程安全:当多个线程同时执行时很难判断你的程序是否正常运行,因为你没有在你的例子中向我们展示任何争用点。 It's very possible that you will experience concurrency issues if your program has contention on MyObject . 如果您的程序在MyObject上存在争用,则可能会遇到并发问题。

Java has the final keyword and C# has a corresponding keyword called readonly , but neither final nor readonly ensure that the state of the object you're modifying will be consistent between threads. Java有final关键字,C#有一个名为readonly的对应关键字,但是finalreadonly都没有确保你修改的对象的状态在线程之间是一致的。 The only thing these keywords do is ensure that you do not change the reference the object is pointing to. 这些关键字唯一能做的就是确保不要更改对象所指向的引用。 If two threads have read/write contention on the same object, then you should perform some type of synchronization or atomic operations on that object in order to ensure thread safety. 如果两个线程在同一个对象上有读/写争用,那么您应该对该对象执行某种类型的同步或原子操作,以确保线程安全。

Update 更新

OK, if you modify the reference to which myObject is pointing to, then your contention is now on myObject . 好的,如果你修改myObject所指向的引用,那么你的争论现在就在myObject I'm sure that my answer will not match your actual situation 100%, but given the example code you've provided I can tell you what will happen: 我确信我的答案与你的实际情况不符100%,但考虑到你提供的示例代码,我可以告诉你会发生什么:

You will not be guaranteed which object gets modified: it can be that.MyObject or this.MyObject . 无法保证哪个对象被修改:它可以是that.MyObjectthis.MyObject That's true regardless if you're working with Java or C#. 无论你是使用Java还是C#,都是如此。 The scheduler may schedule your thread/timer to be executed before, after or during the second assignment. 调度程序可以调度您的线程/计时器在第二次分配之前,之后或期间执行。 If you're counting on a specific order of execution, then you have to do something to ensure that order of execution. 如果你指望一个特定的执行顺序,那么你必须做一些事情来确保执行的顺序。 Usually that something is a communication between the threads in the form of a signal: a ManualResetEvent , Join or something else. 通常, 某些东西是信号形式的线程之间的通信: ManualResetEventJoin或其他东西。

Here is a join example: 这是一个连接示例:

MyObject myObject = this.MyObject;
Thread task = new Thread(()=>
    {
        Thread.Sleep(1000); // wait a second (for a specific reason)
        DoTheCodeThatNeedsToRunAsynchronously();
        myObject.ChangeSomeProperty();
    });
task.IsBackground = true;
task.Start();
task.Join(); // blocks the main thread until the task thread is finished
myObject = that.MyObject; // the assignment will happen after the task is complete

Here is a ManualResetEvent example: 这是一个ManualResetEvent示例:

ManualResetEvent done = new ManualResetEvent(false);
MyObject myObject = this.MyObject;
Thread task = new Thread(()=>
    {
        Thread.Sleep(1000); // wait a second (for a specific reason)
        DoTheCodeThatNeedsToRunAsynchronously();
        myObject.ChangeSomeProperty();
        done.Set();
    });
task.IsBackground = true;
task.Start();
done.WaitOne(); // blocks the main thread until the task thread signals it's done
myObject = that.MyObject; // the assignment will happen after the task is done

Of course, in this case it's pointless to even spawn multiple threads, since you're not going to allow them to run concurrently. 当然,在这种情况下,即使生成多个线程也没有意义,因为你不会允许它们同时运行。 One way to avoid this is by not changing the reference to myObject after you've started the thread, then you won't need to Join or WaitOne on the ManualResetEvent . 避免这种情况的一种方法是在启动线程后不更改对myObject的引用,然后在ManualResetEvent上不需要JoinWaitOne

So this leads me to a question: why are you assigning a new object to myObject ? 所以这引出了一个问题:为什么要为myObject分配一个新对象? Is this a part of a for-loop which is starting multiple threads to perform multiple asynchronous tasks? 这是一个for循环的一部分,它启动多个线程来执行多个异步任务吗?

"Thread-safe" is a tricky beast. “线程安全”是一个棘手的野兽。 With both of your approches, the problem is that the "MyObject" your thread is using may be modified/read by multiple threads in a way that makes the state appear inconsistent, or makes your thread behave in a way inconsistent with actual state. 使用这两种方法,问题是你的线程正在使用的“MyObject”可能被多个线程修改/读取,使得状态看起来不一致,或者使你的线程的行为方式与实际状态不一致。

For example, say your MyObject.ChangeSomeproperty() MUST be called before MyObject.DoSomethingElse() , or it throws. 例如,假设您的MyObject.ChangeSomeproperty()必须在MyObject.DoSomethingElse()之前调用,否则它会抛出。 With either of your approaches, there is nothing to stop any other thread from calling DoSomethingElse() before the thread that will call ChangeSomeProperty() finishes. 使用您的任何一种方法,在调用ChangeSomeProperty()的线程完成之前,没有任何东西可以阻止任何其他线程调用DoSomethingElse()

Or, if ChangeSomeProperty() happens to be called by two threads, and it (internally) changes state, the thread context switch may happen while the first thread is in the middle of it's work and the end result is that the actual new state after both threads is "wrong". 或者,如果ChangeSomeProperty()碰巧被两个线程调用,并且它(内部)改变状态,则线程上下文切换可能在第一个线程处于其工作中间时发生,最终结果是之后的实际新状态两个线程都是“错误的”。

However, by itself, neither of your approaches is inherently thread-unsafe, they just need to make sure that changing state is serialized and that accessing state is always giving a consistent result. 但是,就其本身而言,您的方法本身都不是线程不安全的,它们只需要确保更改状态是序列化的,并且访问状态始终给出一致的结果。

Personally, I wouldn't use the second approach. 就个人而言,我不会使用第二种方法。 If you're having problems with "zombie" threads, set IsBackground to true on the thread. 如果您遇到“僵尸”线程问题,请在线程IsBackground设置为true。

What is the best way to achieve what I want? 实现我想要的最好方法是什么? Is it one of the two or another approach? 它是两种或另一种方法之一吗?

Both look fine, but... 两个看起来很好,但......

Is one of the two ways not thread-safe (I fear both...) and why? 两种方式中的一种不是线程安全的(我担心两者......)以及为什么?

...they are not thread safe unless MyObject.ChangeSomeProperty() is thread safe. ... 除非 MyObject.ChangeSomeProperty()是线程安全的, 否则它们不是线程安全的。

The first approach creates a thread and passes it the object in the constructor. 第一种方法创建一个线程并将其传递给构造函数中的对象。 Is that how I'm supposed to pass the object? 这是我应该如何传递对象?

Yes. 是。 Using a closure (as in your second approach) is fine as well, with the additional advantage that you don't need to do a cast. 使用闭包(如第二种方法)也很好,还有一个额外的好处就是你不需要进行演员表演。

The second approach uses a timer which doesn't provide that possibility, so I just use the local variable in the anonymous delegate. 第二种方法使用不提供这种可能性的计时器,所以我只使用匿名委托中的局部变量。 Is that safe or is it possible in theory that the reference in the variable changes before it is evaluated by the delegate code? 这是安全还是理论上可以在变量中的引用在委托代码评估之前发生变化? (This is a very generic question whenever one uses anonymous delegates). (每当使用匿名代表时,这是一个非常通用的问题)。

Sure, if you add myObject = null; 当然,如果你添加myObject = null; directly after setting timer.Elapsed , then the code in your thread will fail. 设置timer.Elapsed后直接,你的线程中的代码将失败。 But why would you want to do that? 但是你为什么要那样做呢? Note that changing this.MyObject will not affect the variable captured in your thread. 请注意,更改this.MyObject不会影响线程中捕获的变量。


So, how to make this thread-safe? 那么,如何使这个线程安全? The problem is that myObject.ChangeSomeProperty(); 问题是myObject.ChangeSomeProperty(); might run in parallel with some other code that modifies the state of myObject . 可能与修改myObject状态的其他代码并行运行。 There are basically two solutions to that: 基本上有两种解决方案:

Option 1 : Execute myObject.ChangeSomeProperty() in the main UI thead. 选项1 :在主UI thead中执行myObject.ChangeSomeProperty() This is the simplest solution if ChangeSomeProperty is fast. 如果ChangeSomeProperty很快,这是最简单的解决方案。 You can use the Dispatcher (WPF) or Control.Invoke (WinForms) to jump back to the UI thread, but the easiest way is to use a BackgroundWorker : 您可以使用Dispatcher (WPF)或Control.Invoke (WinForms)跳回UI线程,但最简单的方法是使用BackgroundWorker

MyObject myObject = this.MyObject;
var bw = new BackgroundWorker();

bw.DoWork += (sender, args) => {
    // this will happen in a separate thread
    Thread.Sleep(1000);
    DoTheCodeThatNeedsToRunAsynchronously();
}

bw.RunWorkerCompleted += (sender, args) => {
    // We are back in the UI thread here.

    if (args.Error != null)  // if an exception occurred during DoWork,
        MessageBox.Show(args.Error.ToString());  // do your error handling here
    else
        myObject.ChangeSomeProperty();
}

bw.RunWorkerAsync(); // start the background worker

Option 2 : Make the code in ChangeSomeProperty() thread-safe by using the lock keyword (inside ChangeSomeProperty as well as inside any other method modifying or reading the same backing field). 选项2 :使用lock关键字(在ChangeSomeProperty内部以及修改或读取相同支持字段的任何其他方法内部ChangeSomeProperty()使ChangeSomeProperty()的代码成为线程安全的。

The bigger thread-safety concern here, in my mind, may be the 1 second Sleep. 在我看来,这里更大的线程安全问题可能是1秒睡眠。 If this is required in order to synchronize with some other operation (giving it time to complete), then I strongly recommend using a proper synchronization pattern rather than relying on the Sleep. 如果需要这个以便与其他操作同步(给它时间来完成),那么我强烈建议使用正确的同步模式而不是依赖于Sleep。 Monitor.Pulse or AutoResetEvent are two common ways to achieve synchronization. Monitor.PulseAutoResetEvent是实现同步的两种常用方法。 Both should be used carefully, as it's easy to introduce subtle race conditions. 两者都应该谨慎使用,因为它很容易引入微妙的竞争条件。 However, using Sleep for synchronization is a race condition waiting to happen. 但是,使用Sleep进行同步是一种等待发生的竞争条件。

Also, if you want to use a thread (and don't have access to the Task Parallel Library in .NET 4.0), then ThreadPool.QueueUserWorkItem is preferable for short-running tasks. 此外,如果要使用线程(并且无法访问.NET 4.0中的任务并行库),则ThreadPool.QueueUserWorkItem最适合短期运行的任务。 The thread pool threads also won't hang up the application if it dies, as long as there is not some deadlock preventing a non-background thread from dying. 只要没有一些死锁阻止非后台线程死亡,线程池线程也不会挂断应用程序。

One thing not mentioned so far: The choice of threading methods depends heavily on specifically what DoTheCodeThatNeedsToRunAsynchronously() does. 到目前为止还没有提到的一件事:线程方法的选择在很大程度上取决于DoTheCodeThatNeedsToRunAsynchronously()作用。

Different .NET threading approaches are suitable for different requirements. 不同的.NET线程方法适用于不同的要求。 One very large concern is whether this method will complete quickly, or take some time (is it short-lived or long-running?). 一个非常大的问题是这种方法是快速完成还是需要一些时间(是短暂的还是长期的?)。

Some .NET threading mechanisms, like ThreadPool.QueueUserWorkItem() , are for use by short-lived threads. 某些.NET线程机制(如ThreadPool.QueueUserWorkItem()供短期线程使用。 They avoid the expense of creating a thread by using "recycled" threads--but the number of threads it will recycle is limited, so a long-running task shouldn't hog the ThreadPool's threads. 它们通过使用“循环”线程避免了创建线程的费用 - 但它将回收的线程数量是有限的,因此长时间运行的任务不应该占用ThreadPool的线程。

Other options to consider are using: 其他需要考虑的选择是使用:

  • ThreadPool.QueueUserWorkItem() is a convienient means to fire-and-forget small tasks on a ThreadPool thread ThreadPool.QueueUserWorkItem()是一种在ThreadPool线程上发送并忘记小任务的方便方法

  • System.Threading.Tasks.Task is a new feature in .NET 4 which makes small tasks easy to run in async/parallel mode. System.Threading.Tasks.Task是.NET 4中的一项新功能,它使小任务易于在异步/并行模式下运行。

  • Delegate.BeginInvoke() and Delegate.EndInvoke() ( BeginInvoke() will run the code asynchronously, but it's crucial that you ensure EndInvoke() is called as well to avoid potential resource-leaks. It's also based on ThreadPool threads I believe. Delegate.BeginInvoke()Delegate.EndInvoke()BeginInvoke()将异步运行代码,但是确保调用EndInvoke()以避免潜在的资源泄漏也是至关重要的。它也基于我相信的ThreadPool线程。

  • System.Threading.Thread as shown in your example. System.Threading.Thread如您的示例所示。 Threads provide the most control but are also more expensive than the other methods--so they are ideal for long-running tasks or detail-oriented multithreading. 线程提供最多的控制,但也比其他方法更昂贵 - 因此它们非常适合长时间运行的任务或面向细节的多线程。

Overall my personal preference has been to use Delegate.BeginInvoke()/EndInvoke() -- it seems to strike a good balance between control and ease of use. 总的来说,我个人的偏好是使用Delegate.BeginInvoke()/EndInvoke() - 它似乎在控制和易用性之间取得了很好的平衡。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM