调用跨线程事件的最干净方法

82

我发现.NET事件模型常常出现在一个线程上触发一个事件,而另一个线程监听这个事件。我想知道将后台线程的事件调度到我的UI线程的最佳方式是什么。

根据社区的建议,我使用了以下方法:

// earlier in the code
mCoolObject.CoolEvent+= 
           new CoolObjectEventHandler(mCoolObject_CoolEvent);
// then
private void mCoolObject_CoolEvent(object sender, CoolObjectEventArgs args)
{
    if (InvokeRequired)
    {
        CoolObjectEventHandler cb =
            new CoolObjectEventHandler(
                mCoolObject_CoolEvent);
        Invoke(cb, new object[] { sender, args });
        return;
    }
    // do the dirty work of my method here
}

请记住,当现有的托管控件尚未具有非托管句柄时,InvokeRequired 可能会返回 false。您应该在完全创建控件之前引发事件时谨慎行事。 - GregC
10个回答

46

我有在线代码这里。它比其他建议都要好得多,一定要看看。

示例用法:

private void mCoolObject_CoolEvent(object sender, CoolObjectEventArgs args)
{
    // You could use "() =>" in place of "delegate"; it's a style choice.
    this.Invoke(delegate
    {
        // Do the dirty work of my method here.
    });
}

你也可以将命名空间更改为 System.Windows.Forms,这样你就可以避免每次需要时都添加 _your custom namespace_ - Joe Almore

29

以下是一些观察:

  • 除非你使用的是2.0以下版本,否则不要在代码中显式创建简单委托:
   BeginInvoke(new EventHandler<CoolObjectEventArgs>(mCoolObject_CoolEvent), 
               sender, 
               args);
  • 此外,您不需要创建和填充对象数组,因为args参数是“params”类型,所以您可以只传递列表。

  • 我可能更喜欢使用Invoke而不是BeginInvoke,因为后者将导致代码被异步调用,这可能是你想要的,也可能不是你想要的,但如果没有调用EndInvoke,处理后续异常将变得困难。结果是,您的应用程序最终会收到一个TargetInvocationException


12

我避免重复的委托声明。

private void mCoolObject_CoolEvent(object sender, CoolObjectEventArgs args)
{
    if (InvokeRequired)
    {
        Invoke(new Action<object, CoolObjectEventArgs>(mCoolObject_CoolEvent), sender, args);
        return;
    }
    // do the dirty work of my method here
}

对于非事件,您可以使用System.Windows.Forms.MethodInvoker委托或System.Action

编辑:此外,每个事件都有一个相应的EventHandler委托,因此根本不需要重新声明一个。


6

我为了自己的目的编写了下面这个“通用”的跨线程调用类,但我认为值得分享:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;

namespace CrossThreadCalls
{
  public static class clsCrossThreadCalls
  {
    private delegate void SetAnyPropertyCallBack(Control c, string Property, object Value);
    public static void SetAnyProperty(Control c, string Property, object Value)
    {
      if (c.GetType().GetProperty(Property) != null)
      {
        //The given property exists
        if (c.InvokeRequired)
        {
          SetAnyPropertyCallBack d = new SetAnyPropertyCallBack(SetAnyProperty);
          c.BeginInvoke(d, c, Property, Value);
        }
        else
        {
          c.GetType().GetProperty(Property).SetValue(c, Value, null);
        }
      }
    }

    private delegate void SetTextPropertyCallBack(Control c, string Value);
    public static void SetTextProperty(Control c, string Value)
    {
      if (c.InvokeRequired)
      {
        SetTextPropertyCallBack d = new SetTextPropertyCallBack(SetTextProperty);
        c.BeginInvoke(d, c, Value);
      }
      else
      {
        c.Text = Value;
      }
    }
  }

您可以在其他线程中使用SetAnyProperty():

CrossThreadCalls.clsCrossThreadCalls.SetAnyProperty(lb_Speed, "Text", KvaserCanReader.GetSpeed.ToString());

在这个例子中,上面的KvaserCanReader类运行它自己的线程,并通过调用设置主窗体上lb_Speed标签的text属性。

3

我认为最干净的方法是采用AOP方式。创建一些方面,添加必要的属性,您就不必再次检查线程亲和性。


我不理解你的建议。C#不是本质上面向方面的语言。你有没有想过一些模式或库来实现在幕后实现封送的方面? - Eric
我使用PostSharp,所以我在属性类中定义线程行为,然后在每个必须在UI线程上调用的方法前面使用[WpfThread]属性。 - Dmitri Nesteruk

3

如果您想将结果发送到UI线程,请使用同步上下文。我需要更改线程优先级,所以我改用自己创建的新线程(注释掉的代码),而不是使用线程池线程。即使这样,我仍然能够使用同步上下文返回数据库取消是否成功。

    #region SyncContextCancel

    private SynchronizationContext _syncContextCancel;

    /// <summary>
    /// Gets the synchronization context used for UI-related operations.
    /// </summary>
    /// <value>The synchronization context.</value>
    protected SynchronizationContext SyncContextCancel
    {
        get { return _syncContextCancel; }
    }

    #endregion //SyncContextCancel

    public void CancelCurrentDbCommand()
    {
        _syncContextCancel = SynchronizationContext.Current;

        //ThreadPool.QueueUserWorkItem(CancelWork, null);

        Thread worker = new Thread(new ThreadStart(CancelWork));
        worker.Priority = ThreadPriority.Highest;
        worker.Start();
    }

    SQLiteConnection _connection;
    private void CancelWork()//object state
    {
        bool success = false;

        try
        {
            if (_connection != null)
            {
                log.Debug("call cancel");
                _connection.Cancel();
                log.Debug("cancel complete");
                _connection.Close();
                log.Debug("close complete");
                success = true;
                log.Debug("long running query cancelled" + DateTime.Now.ToLongTimeString());
            }
        }
        catch (Exception ex)
        {
            log.Error(ex.Message, ex);
        }

        SyncContextCancel.Send(CancelCompleted, new object[] { success });
    }

    public void CancelCompleted(object state)
    {
        object[] args = (object[])state;
        bool success = (bool)args[0];

        if (success)
        {
            log.Debug("long running query cancelled" + DateTime.Now.ToLongTimeString());

        }
    }

2

我一直想知道,总是假设需要调用invoke会有多大的成本...

private void OnCoolEvent(CoolObjectEventArgs e)
{
  BeginInvoke((o,e) => /*do work here*/,this, e);
}

3
在GUI线程中执行BeginInvoke将导致所涉及的操作被推迟到下一次UI线程处理Windows消息时执行。在某些情况下,这实际上是一个有用的做法。 - supercat

2
作为一个有趣的旁注,WPF的绑定处理自动进行了marshaller,因此您可以将UI绑定到后台线程上修改的对象属性,而无需做任何特殊处理。这对我来说已经证明是一个极大的时间节省。
在XAML中:
<TextBox Text="{Binding Path=Name}"/>

这样做行不通。一旦在非 UI 线程上设置了属性,就会出现异常。例如 Name="gbc",嘭!失败了……免费的奶酪是没有的,伙计。 - Boppity Bop
它并非免费的(需要执行时间),但 WPF 绑定机制似乎能够自动处理跨线程调度。我们经常使用它来更新由后台线程接收到的网络数据所更新的属性。这里有一个说明:http://blog.lab49.com/archives/1166 - gbc
1
@gbc 啊啊啊,解释已经消失了404。 - Jan 'splite' K.

0

你可以尝试开发一种通用组件,它接受一个SynchronizationContext作为输入,并使用它来调用事件。


-3

我正在使用类似于

Invoke((Action)(() =>
        {
            //your code
        }));

1
请在您的答案中添加上下文。 - Lukasz Szczygielek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接