C#multithreading – 无控件调用

我对multithreading有点熟悉,因为我已经读过它,但从未在实践中使用它。

我有一个项目使用第三方库,通过引发事件来共享输入设备的状态。 问题是,编写库的方式这些事件是从不同的线程引发的。

我的应用程序不需要是multithreading的,我遇到了很多经典的线程问题(UI控件抱怨从不同的线程进行交互,集合被修改为一段代码迭代它,等等)。

我只想将第三方库的事件返回给我的UI线程。 具体我认为应该发生的是:

我的类接收事件,处理程序正在与UI不同的线程上运行。 我想检测这种情况(比如使用InvokeRequired),然后执行BeginInvoke的等效操作以将控制权交还给UI线程。 然后可以在类层次结构上发送正确的通知,并且只有一个线程触及我的所有数据。

问题是,接收这些输入事件的类不是从Control派生的,因此没有InvokeRequired或BeginInvoke。 原因是我试图干净地分离UI和底层逻辑。 该类仍然在UI线程上运行,它在类本身内部没有任何UI。

现在我通过破坏这种分离来解决问题。 我传入一个引用控件,该控件将显示来自我的类​​并使用 Invoke方法的数据。 这似乎违背了分离它们的整个目的,因为现在底层类直接依赖于我的特定UI类。

也许有一种方法可以保存对运行构造函数的线程的引用,然后在Threading命名空间中会有一些执行Invoke命令的东西?

有没有解决的办法? 我的方法完全错了吗?

查看AsyncOperation类。 您可以使用AsyncOperationManager.CreateOperation方法在要调用处理程序的线程上创建AsyncOperation的实例。 我用于Create的参数通常为null,但您可以将其设置为任何值。 要在该线程上调用方法,请使用AsyncOperation.Post方法。

使用SynchronizationContext.Current ,它将指向您可以与之同步的内容。

根据应用类型,这将做正确的事情 。 对于WinForms应用程序,它将在主UI线程上运行它。

具体来说,使用SynchronizationContext.Send方法,如下所示:

 SynchronizationContext context = SynchronizationContext.Current ?? new SynchronizationContext(); context.Send(s => { // your code here }, null); 

处理方法可以简单地将数据存储到类的成员变量中。 当您想要将线程更新为未在该线程上下文中创建的控件时,会出现跨线程的唯一问题。 因此,您的generics类可以侦听事件,然后使用委托函数调用要更新的实际控件。

同样,只需要调用要更新的UI控件以使其线程安全。 前一段时间我写了一篇关于“ C#中非法跨线程调用的简单解决方案 ”的博客文章

这篇文章更详细,但一个非常简单(但有限)的方法的关键是在你想要更新的UI控件上使用匿名委托函数:

 if (label1.InvokeRequired) { label1.Invoke( new ThreadStart(delegate { label1.Text = "some text changed from some thread"; })); } else { label1.Text = "some text changed from the form's thread"; } 

我希望这有帮助。 InvokeRequired在技术上是可选的,但是调用控件非常昂贵,因此检查确保它不会在不需要时通过调用更新label1.Text。

您不需要特定控件,任何控件(包括Form)都可以。 所以你可以在某种程度上将它从UI中抽象出来。

如果您使用的是WPF:

您需要对管理UI线程的Dispatcher对象的引用。 然后,您可以在调度程序对象上使用Invoke或BeginInvoke方法来计划在UI线程中发生的操作。

获取调度程序的最简单方法是使用Application.Current.Dispatcher。 这是负责主要(可能是唯一的)UI线程的调度程序。

把它们放在一起:

 class MyClass { // Can be called on any thread public ReceiveLibraryEvent(RoutedEventArgs e) { if (Application.Current.CheckAccess()) { this.ReceiveLibraryEventInternal(e); } else { Application.Current.Dispatcher.Invoke( new Action(this.ReceiveLibraryEventInternal)); } } // Must be called on the UI thread private ReceiveLibraryEventInternal(RoutedEventArgs e) { // Handle event } } 

有没有解决的办法?

是的,解决方法是为您创建一个线程安全的队列。

  • 您的事件处理程序由第三方线程调用
  • 您的事件处理程序将某些内容(事件数据)排入您拥有的集合(例如List)
  • 你的事件处理程序会发出一些信号来指示你自己的thead,集合中的数据是为了出列并处理:
    • 你的线程可能正在等待一些东西(互斥或其他); 当它的互斥锁由事件处理程序发出信号时,它会唤醒并检查队列。
    • 或者,它不是被发信号通知,而是可以周期性地唤醒(例如每秒一次或其他)并轮询队列。

在任何一种情况下,由于您的队列由两个不同的线程编写(第三方线程入队,并且您的线程已出队),因此它需要是一个线程安全的受保护队列。

我刚遇到同样的情况。 但是,在我的情况下,我无法使用SynchronizationContext.Current,因为我无法访问任何UI组件,也没有回调来捕获当前的同步上下文。 事实certificate,如果代码当前没有在Windows Forms消息泵中运行,SynchronizationContext.Current将被设置为标准的SynchronizationContext,它将在当前线程上运行Send调用并在ThreadPool上运行Post调用。

我发现这个答案解释了不同类型的同步上下文。 在我的例子中,解决方案是在线程上创建一个新的WindowsFormsSynchronizationContext对象,该对象稍后将使用Application.Run()启动消息泵。 然后,其他线程可以使用此同步上下文在UI线程上运行代码,而无需触及任何UI组件。