确保异步方法能够更新非线程安全集合的最佳方法是什么?

时间:2022-11-16 18:08:07

I'm doing async calls that eventually will update a collection in the GUI. The async call is done from a delegate command like this:

我正在进行异步调用,最终将更新GUI中的集合。异步调用是由这样的委托命令完成的:

StartDoingUsefulStuffCommand = new DelegateCommand(() => Task.Run(() => StartDoingUsefulStuff()));

public async Task StartDoingUsefulStuff()
{
    try
    {
        await some method
        do something else
        MyCollection.Clear();
        ...
        ...
    }
    catch (Exception e)
    {
       // handle exception
    }
}

Eventually the StartDoingUsefulStuff method wants to update a collection, resulting in this exception because another thread tries to update a collection:

最终startdoingfunctionstuff方法想要更新集合,导致此异常,因为另一个线程试图更新集合:

A first chance exception of type 'System.NotSupportedException' occurred in PresentationFramework.dll

类型系统的第一次机会异常。NotSupportedException”由于发生在PresentationFramework.dll

Additional information: This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.

附加信息:这种类型的CollectionView不支持从与Dispatcher线程不同的线程对其源聚合的更改。

I've found one solution: Run the task in the current SynchronizationContext:

我找到了一个解决方案:在当前SynchronizationContext中运行任务:

Task.Factory.StartNew(() => StartDoingUsefulStuff(), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());

This will work fine, but to me it seems tedious to do all this stuff each time a command is called from XAML. What are your opinions on my present solution/what are best practices?

这可以很好地工作,但是对我来说,每次从XAML调用命令时都要做所有这些工作似乎很乏味。你对我现在的解决方案有什么看法?

4 个解决方案

#1


3  

You've already given the correct answer. The problem here is not that the collection is not thread-safe. The problem is that the collection cannot be modified by a thread other than the UI thread.

你已经给出了正确的答案。这里的问题不是集合不是线程安全的。问题是,除了UI线程之外,集合不能被其他线程修改。

This is a common 'issue' with UI controls, whether it's WPF or WinForms.

这是一个常见的“问题”,UI控件,无论是WPF还是WinForms。

The solution is indeed to pass the required options to run the task in the 'current synchronization context' which means (in WPF) that the completions are run in the thread that calls them, which is usually the UI thread.

解决方案实际上是传递在“当前同步上下文”中运行任务所需的选项,这意味着(在WPF中)完成任务在调用它们的线程中运行,通常是UI线程。

You don't have to do this all the time. In fact, you're only doing it once, when you manually start the new task. The C# 5 'async' behavior by default does this too, which is why most of us have the tedious task of doing the reverse: tell the scheduler not to use the same synchronization context with the following code:

你不需要一直这样做。实际上,当您手动启动新任务时,您只执行一次。c# 5 'async'行为在默认情况下也是这样的,这就是为什么我们大多数人都要做相反的冗长工作:告诉调度器不要使用与以下代码相同的同步上下文:

var result = await SomeAsync().ConfigureAwait(false);

There are issues to using the default behavior, including locking up your main thread, but in your case, it's exactly that what you want.

使用默认行为有一些问题,包括锁定主线程,但是在您的例子中,这正是您想要的。

Maybe you should consider not calling Task.Run() but instead just call the async method directly (without Task.Run()) and discard the returned task object (i.e. fire-and-forget).

也许您应该考虑不调用task . run(),而是直接调用async方法(没有task . run())),并丢弃返回的任务对象(即fire-and-forget)。

#2


4  

There is one more simple way to fix it. In the constructor where your Collection is intialized just add the below line after initialization.

还有一种更简单的方法来修复它。在初始化集合的构造函数中,只需在初始化后添加以下行。

BindingOperations.EnableCollectionSynchronization(MyCollection,_lock); where _lock is a private static object. You don't have to take care of thread safety in your commands anymore then. You can then use the await Task.Run safely.

BindingOperations.EnableCollectionSynchronization(MyCollection _lock);其中_lock是一个私有静态对象。这样,您就不必再在命令中关注线程安全了。然后您可以使用等待任务。安全运行。

#3


0  

Use the dispatcher object of "MyCollection" Something like this:

使用“MyCollection”的dispatcher对象如下:

MyCollection.Dispatcher.BeginInvoke((Action)(() => 
{ 
    MyCollection.Clear()) 
}));

#4


0  

Some objects in .NET have thread affinity, typically to the thread that they were created on. This is why you are getting the NotSupportedException, because you're trying to alter an object with thread affinity from the wrong thread.

. net中的一些对象具有线程关联,通常与创建它们的线程关联。这就是为什么要获得NotSupportedException,因为您试图从错误的线程中修改具有线程亲和力的对象。

Essentially you need to make sure that any operation which may run into issues because of thread affinity is executed on the correct thread.

本质上,您需要确保在正确的线程上执行任何可能由于线程关联而出现问题的操作。

You can leverage the Dispatcher object for an easy way to execute arbitrary code on a specific thread. If you are using ViewModels, you can require that they be injected with a Dispatcher (i.e. as a constructor parameter), representing the Dispatcher that the objects with thread affinity were created on.

您可以利用Dispatcher对象来实现在特定线程上执行任意代码的简单方法。如果您正在使用viewmodel,您可以要求将它们注入一个Dispatcher(例如,作为一个构造函数参数),表示创建线程关联对象的Dispatcher。

public abstract class ViewModelBase
{
    public ViewModelBase(Dispatcher dispatcher)
    {
        UIDispatcher = dispatcher;
    }

    protected Dispatcher UIDispatcher;
}

Whenever you need to execute a piece of code that must occur on the UI thread, you can leverage the Dispatcher, and use the Invoke or BeginInvoke methods as appropriate.

当您需要执行UI线程上必须执行的代码时,您可以利用Dispatcher,并根据需要使用Invoke或BeginInvoke方法。

Keeping a Dispatcher around seems to be the way that Microsoft deals with objects with thread affinity too. The CollectionView object that you are using has a Dispatcher property (inherited from DispatcherObject) that you can use to execute code on the appropriate thread.

保持一个调度程序似乎也是微软处理具有线程关联的对象的方式。您正在使用的CollectionView对象具有Dispatcher属性(继承自DispatcherObject),可以使用该属性在适当的线程上执行代码。

MyCollection.Dispatcher.Invoke(new Action(() => MyCollection.Clear()));

I would recommend not using TaskScheduler.FromCurrentSynchronizationContext(), unless you are absolutely certain that the operation will not require any meaningful amount of processing time. You want to keep as many operations off the UI thread as possible, in order to maintain a responsive UI. Using Tasks is a good way to do this, but using the TaskScheduler created from the UI thread is definitely the wrong way to solve the thread affinity problem, assuming that maintaining a responsive UI is important.

我建议不要使用TaskScheduler.FromCurrentSynchronizationContext(),除非您绝对确信操作不需要任何有意义的处理时间。您希望尽可能多地在UI线程之外执行操作,以便维护响应良好的UI。使用任务是一种很好的方法,但是使用UI线程创建的TaskScheduler绝对是解决线程关联问题的错误方法,假设保持响应性UI非常重要。

#1


3  

You've already given the correct answer. The problem here is not that the collection is not thread-safe. The problem is that the collection cannot be modified by a thread other than the UI thread.

你已经给出了正确的答案。这里的问题不是集合不是线程安全的。问题是,除了UI线程之外,集合不能被其他线程修改。

This is a common 'issue' with UI controls, whether it's WPF or WinForms.

这是一个常见的“问题”,UI控件,无论是WPF还是WinForms。

The solution is indeed to pass the required options to run the task in the 'current synchronization context' which means (in WPF) that the completions are run in the thread that calls them, which is usually the UI thread.

解决方案实际上是传递在“当前同步上下文”中运行任务所需的选项,这意味着(在WPF中)完成任务在调用它们的线程中运行,通常是UI线程。

You don't have to do this all the time. In fact, you're only doing it once, when you manually start the new task. The C# 5 'async' behavior by default does this too, which is why most of us have the tedious task of doing the reverse: tell the scheduler not to use the same synchronization context with the following code:

你不需要一直这样做。实际上,当您手动启动新任务时,您只执行一次。c# 5 'async'行为在默认情况下也是这样的,这就是为什么我们大多数人都要做相反的冗长工作:告诉调度器不要使用与以下代码相同的同步上下文:

var result = await SomeAsync().ConfigureAwait(false);

There are issues to using the default behavior, including locking up your main thread, but in your case, it's exactly that what you want.

使用默认行为有一些问题,包括锁定主线程,但是在您的例子中,这正是您想要的。

Maybe you should consider not calling Task.Run() but instead just call the async method directly (without Task.Run()) and discard the returned task object (i.e. fire-and-forget).

也许您应该考虑不调用task . run(),而是直接调用async方法(没有task . run())),并丢弃返回的任务对象(即fire-and-forget)。

#2


4  

There is one more simple way to fix it. In the constructor where your Collection is intialized just add the below line after initialization.

还有一种更简单的方法来修复它。在初始化集合的构造函数中,只需在初始化后添加以下行。

BindingOperations.EnableCollectionSynchronization(MyCollection,_lock); where _lock is a private static object. You don't have to take care of thread safety in your commands anymore then. You can then use the await Task.Run safely.

BindingOperations.EnableCollectionSynchronization(MyCollection _lock);其中_lock是一个私有静态对象。这样,您就不必再在命令中关注线程安全了。然后您可以使用等待任务。安全运行。

#3


0  

Use the dispatcher object of "MyCollection" Something like this:

使用“MyCollection”的dispatcher对象如下:

MyCollection.Dispatcher.BeginInvoke((Action)(() => 
{ 
    MyCollection.Clear()) 
}));

#4


0  

Some objects in .NET have thread affinity, typically to the thread that they were created on. This is why you are getting the NotSupportedException, because you're trying to alter an object with thread affinity from the wrong thread.

. net中的一些对象具有线程关联,通常与创建它们的线程关联。这就是为什么要获得NotSupportedException,因为您试图从错误的线程中修改具有线程亲和力的对象。

Essentially you need to make sure that any operation which may run into issues because of thread affinity is executed on the correct thread.

本质上,您需要确保在正确的线程上执行任何可能由于线程关联而出现问题的操作。

You can leverage the Dispatcher object for an easy way to execute arbitrary code on a specific thread. If you are using ViewModels, you can require that they be injected with a Dispatcher (i.e. as a constructor parameter), representing the Dispatcher that the objects with thread affinity were created on.

您可以利用Dispatcher对象来实现在特定线程上执行任意代码的简单方法。如果您正在使用viewmodel,您可以要求将它们注入一个Dispatcher(例如,作为一个构造函数参数),表示创建线程关联对象的Dispatcher。

public abstract class ViewModelBase
{
    public ViewModelBase(Dispatcher dispatcher)
    {
        UIDispatcher = dispatcher;
    }

    protected Dispatcher UIDispatcher;
}

Whenever you need to execute a piece of code that must occur on the UI thread, you can leverage the Dispatcher, and use the Invoke or BeginInvoke methods as appropriate.

当您需要执行UI线程上必须执行的代码时,您可以利用Dispatcher,并根据需要使用Invoke或BeginInvoke方法。

Keeping a Dispatcher around seems to be the way that Microsoft deals with objects with thread affinity too. The CollectionView object that you are using has a Dispatcher property (inherited from DispatcherObject) that you can use to execute code on the appropriate thread.

保持一个调度程序似乎也是微软处理具有线程关联的对象的方式。您正在使用的CollectionView对象具有Dispatcher属性(继承自DispatcherObject),可以使用该属性在适当的线程上执行代码。

MyCollection.Dispatcher.Invoke(new Action(() => MyCollection.Clear()));

I would recommend not using TaskScheduler.FromCurrentSynchronizationContext(), unless you are absolutely certain that the operation will not require any meaningful amount of processing time. You want to keep as many operations off the UI thread as possible, in order to maintain a responsive UI. Using Tasks is a good way to do this, but using the TaskScheduler created from the UI thread is definitely the wrong way to solve the thread affinity problem, assuming that maintaining a responsive UI is important.

我建议不要使用TaskScheduler.FromCurrentSynchronizationContext(),除非您绝对确信操作不需要任何有意义的处理时间。您希望尽可能多地在UI线程之外执行操作,以便维护响应良好的UI。使用任务是一种很好的方法,但是使用UI线程创建的TaskScheduler绝对是解决线程关联问题的错误方法,假设保持响应性UI非常重要。