C#多线程 - 无控件调用

时间:2009-11-15 23:30:44

标签: c# multithreading invoke

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

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

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

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

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

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

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

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

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

7 个答案:

答案 0 :(得分:18)

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

答案 1 :(得分:13)

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

这将做正确的事™,具体取决于应用程序的类型。对于WinForms应用程序,它将在主UI线程上运行它。

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

SynchronizationContext context =
    SynchronizationContext.Current ?? new SynchronizationContext();

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

答案 2 :(得分:2)

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

同样,只需要调用要更新的UI控件以使其线程安全。不久前,我在“Simple Solution to Illegal Cross-thread Calls in 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。

答案 3 :(得分:1)

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

答案 4 :(得分:1)

如果您使用的是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<RoutedEventArgs>(this.ReceiveLibraryEventInternal));
        }
    }

    // Must be called on the UI thread
    private ReceiveLibraryEventInternal(RoutedEventArgs e)
    {
         // Handle event
    }
}

答案 5 :(得分:0)

  

有解决方法吗?

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

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

在任何一种情况下,因为你的队列是由两个不同的线程编写的(第三方线程是入队的,你的线程是出列的),所以它需要是一个线程安全的受保护队列。

答案 6 :(得分:0)

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

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