【问题标题】:C# Multithreading -- Invoke without a ControlC# 多线程——不带控件的调用
【发布时间】:2009-11-15 23:30:43
【问题描述】:

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

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

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

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

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

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

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

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

有没有办法解决这个问题?我的方法完全错误吗?

【问题讨论】:

    标签: c# multithreading invoke


    【解决方案1】:

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

    【讨论】:

    • 我花了一段时间才了解如何使用代表,但它确实有效!
    【解决方案2】:

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

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

    具体来说,使用SynchronizationContext.Send方法,像这样:

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

    【讨论】:

    • +1。请注意,如果程序是控制台应用程序,SynchronizationContext.Current 为 null。一个好的模式是使用AsyncOperation,它在创建时捕获当前的SynchronizationContext,并且*做正确的事情++*™。 msdn.microsoft.com/en-us/library/…
    • 它是 WinForms,所以它可以工作,但我的意图是独立于 UI。控制台应用程序正在推动它的 UI 方面我会考虑但很高兴知道它不会工作。还是 +1
    【解决方案3】:

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

    同样,只有您想要更新的 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。

    【讨论】:

    • 我的问题更像是:来自库的工作线程通知我的应用程序,我想立即采取行动。问题是“立即操作”将在工作线程上运行,完全隐藏 UI 线程上发生的其他事情(与 UI 无关的事情——例如枚举对象列表)。
    【解决方案4】:

    您不需要特定的控件,任何控件(包括表单)都可以。所以你可以将它从 UI 中抽象出来。

    【讨论】:

      【解决方案5】:

      如果您使用的是 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
          }
      }
      

      【讨论】:

      • 这是关于 WPF 的,我看不出问题在哪里使用。
      【解决方案6】:

      有没有办法解决这个问题?

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

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

      在任何一种情况下,由于您的队列是由两个不同的线程写入的(第 3 方线程正在入队,而您的线程正在出队),因此它必须是线程安全的受保护队列。

      【讨论】:

      • 我看到了很多类似的例子。我可能是错的,但是当出队是不能进入睡眠并且不能坐在那里循环轮询队列的主 UI 线程时,这种技术不会起作用。我没有提到这一点,但另一个线程每秒会引发很多次事件。
      • 你说得对:只有当你想要调用的“你的”线程不是 UI 线程时,这才是合适的。
      【解决方案7】:

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-12-22
        • 2016-11-30
        • 1970-01-01
        • 1970-01-01
        • 2010-11-18
        • 1970-01-01
        相关资源
        最近更新 更多