调用跨线程事件的最简洁方式

Cleanest Way to Invoke Cross-Thread Events

提问人:Nick 提问时间:8/22/2008 最后编辑:Nick 更新时间:3/15/2021 访问量:123877

问:

我发现 .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
}
C# 多线程事件

评论

0赞 GregC 4/30/2009
请记住,当现有托管控件尚无非托管句柄时,InvokeRequired 可能会返回 false。在完全创建控制权之前,您应该谨慎对待将要引发的事件。

答:

12赞 Konrad Rudolph 8/22/2008 #1

我避免了多余的委托声明。

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.MethodInvokerSystem.Action

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

2赞 user1228 8/22/2008 #2

我一直在想,总是假设需要调用的成本有多高......

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

评论

3赞 supercat 4/18/2011
在 GUI 线程中执行 BeginInvoke 将导致相关操作推迟到下次 UI 线程处理 Windows 消息时。在某些情况下,这实际上是一件有用的事情。
29赞 Shaun Austin 8/22/2008 #3

几点观察:

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

  • 我可能会赞成,因为后者将导致代码被异步调用,这可能是也可能不是你所追求的,但会使处理后续异常变得难以在没有调用的情况下传播。发生的情况是,您的应用程序最终会得到一个。InvokeBeginInvokeEndInvokeTargetInvocationException

0赞 On Freund 8/22/2008 #4

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

2赞 gbc 8/22/2008 #5

有趣的是,WPF 的绑定会自动处理封送处理,因此您可以将 UI 绑定到在后台线程上修改的对象属性,而无需执行任何特殊操作。事实证明,这对我来说是一个很大的节省时间。

在 XAML 中:

<TextBox Text="{Binding Path=Name}"/>

评论

0赞 Boppity Bop 11/23/2011
这是行不通的。一旦你在非UI线程上设置了道具,你就会得到异常。即 Name=“gbc” 砰!失败。。。没有免费的奶酪伴侣
0赞 gbc 11/28/2011
它不是免费的(它需要执行时间),但 wpf 绑定机制似乎可以自动处理跨线程封送处理。我们经常将其用于由后台线程上接收的网络数据更新的道具。这里有一个解释:blog.lab49.com/archives/1166
1赞 Jan 'splite' K. 9/11/2017
@gbc Aaaaand 解释消失了 404。
46赞 Domenic 11/3/2008 #6

我在网上有一些代码。它比其他建议要好得多;一定要检查一下。

用法示例:

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.
    });
}

评论

0赞 Joe Almore 2/2/2015
还可以在扩展中更改命名空间。这样,您就可以避免每次需要时都添加自定义命名空间System.Windows.Forms
3赞 Dmitri Nesteruk 4/30/2009 #7

我认为最干净的方法肯定是走AOP路线。做几个方面,添加必要的属性,你就再也不用检查线程亲和性了。

评论

0赞 Eric 5/6/2009
我不明白你的建议。C# 不是面向本机方面的语言。您是否想到了一些模式或库,用于实现在后台实现封送处理的方面?
0赞 Dmitri Nesteruk 5/6/2009
我使用 PostSharp,因此我在属性类中定义线程行为,然后在必须在 UI 线程上调用的每个方法前面使用 [WpfThread] 属性。
6赞 TarPista 10/5/2012 #8

出于我自己的目的,我制作了以下“通用”跨线程调用类,但我认为值得分享:

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赞 The Lonely Coder 7/7/2015 #9

如果要将结果发送到 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());

        }
    }
-3赞 mahirgul 3/15/2021 #10

我正在使用类似的东西

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

评论

1赞 Lukasz Szczygielek 3/15/2021
请在您的回答中添加上下文。