【问题标题】:Automating the InvokeRequired code pattern自动化 InvokeRequired 代码模式
【发布时间】:2011-01-22 23:42:50
【问题描述】:

我已经痛苦地意识到需要在事件驱动的 GUI 代码中编写以下代码模式的频率,其中

private void DoGUISwitch() {
    // cruisin for a bruisin' through exception city
    object1.Visible = true;
    object2.Visible = false;
}

变成:

private void DoGUISwitch() {
    if (object1.InvokeRequired) {
        object1.Invoke(new MethodInvoker(() => { DoGUISwitch(); }));
    } else {
        object1.Visible = true;
        object2.Visible = false;
    }
}

这是 C# 中的一个尴尬模式,无论是记忆还是打字。有没有人想出某种捷径或结构在一定程度上实现自动化?如果有一种方法可以将函数附加到执行此检查的对象而无需执行所有这些额外工作,例如object1.InvokeIfNecessary.visible = true 类型快捷方式,那就太棒了。

以前的answers 已经讨论过每次只调用 Invoke() 的不切实际,即使这样 Invoke() 语法既低效又仍然难以处理。

那么,有没有人想出捷径呢?

【问题讨论】:

  • 我也想知道同样的事情,但是关于 WPF 的 Dispatcher.CheckAccess()。
  • 受您的object1.InvokeIfNecessary.Visible = true 行的启发,我想出了一个相当疯狂的建议;查看我更新的答案,让我知道你的想法。
  • 添加一个片段来帮助实施马特戴维斯建议的方法:见我的回答(迟到但只是向后来的读者展示如何;-))
  • 我不明白为什么微软没有在 .NET 中简化它。从线程中为表单上的每个更改创建委托真的很烦人。
  • @Kamil 我完全同意!鉴于其无处不在,这是一种疏忽。在框架内,如有必要,只需处理线程即可。看起来很明显。

标签: c# multithreading winforms thread-safety invokerequired


【解决方案1】:

你可以写一个扩展方法:

public static void InvokeIfRequired(this Control c, Action<Control> action)
{
    if(c.InvokeRequired)
    {
        c.Invoke(new Action(() => action(c)));
    }
    else
    {
        action(c);
    }
}

并像这样使用它:

object1.InvokeIfRequired(c => { c.Visible = true; });

编辑:正如 Simpzon 在 cmets 中指出的那样,您也可以将签名更改为:

public static void InvokeIfRequired<T>(this T c, Action<T> action) 
    where T : Control

【讨论】:

  • 也许我太笨了,但是这段代码不会编译。所以我修复了它,因为它由我构建(VS2008)。
  • 只是为了完整性:在 WPF 中有不同的调度机制,但它的工作原理相当相似。您可以在那里使用此扩展方法: public static void InvokeIfRequired(this T aTarget, Action aActionToExecute) where T:DispatcherObject { if (aTarget.CheckAccess()) { aActionToExecute(aTarget); } else { aTarget.Dispatcher.Invoke(aActionToExecute); } }
  • 我添加了一个稍微简化了 Lee 的解决方案的答案。
  • 嗨,当我使用类似的东西时,这个通用实现可能会出现一个大问题。如果控件是 Disposing/Disposed,你会得到一个 ObjectDisposedException。
  • @Offler - 好吧,如果它们被放置在不同的线程上,你就有同步问题,这不是这个方法的问题。
【解决方案2】:

这是我在所有代码中一直使用的表单。

private void DoGUISwitch()
{ 
    Invoke( ( MethodInvoker ) delegate {
        object1.Visible = true;
        object2.Visible = false;
    });
} 

我是根据博客条目here 编写的。我没有让这种方法失败,所以我认为没有理由通过检查 InvokeRequired 属性来使我的代码复杂化。

希望这会有所帮助。

【讨论】:

  • +1 - 我偶然发现了你所做的同一篇博客文章,并认为这是所有建议中最干净的方法
  • 使用这种方法对性能的影响很小,多次调用时可能会堆积起来。 stackoverflow.com/a/747218/724944
  • 如果代码可以在控件显示之前执行,则必须使用InvokeRequired,否则会出现致命异常。
【解决方案3】:

创建一个 ThreadSafeInvoke.sn-p 文件,然后你可以只选择更新语句,右键单击并选择 'Surround With...' 或 Ctrl-K+S:

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippet Format="1.0.0" xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <Header>
    <Title>ThreadsafeInvoke</Title>
    <Shortcut></Shortcut>
    <Description>Wraps code in an anonymous method passed to Invoke for Thread safety.</Description>
    <SnippetTypes>
      <SnippetType>SurroundsWith</SnippetType>
    </SnippetTypes>
  </Header>
  <Snippet>
    <Code Language="CSharp">
      <![CDATA[
      Invoke( (MethodInvoker) delegate
      {
          $selected$
      });      
      ]]>
    </Code>
  </Snippet>
</CodeSnippet>

【讨论】:

    【解决方案4】:

    Lee 的方法可以进一步简化

    public static void InvokeIfRequired(this Control control, MethodInvoker action)
    {
        // See Update 2 for edits Mike de Klerk suggests to insert here.
    
        if (control.InvokeRequired) {
            control.Invoke(action);
        } else {
            action();
        }
    }
    

    并且可以这样调用

    richEditControl1.InvokeIfRequired(() =>
    {
        // Do anything you want with the control here
        richEditControl1.RtfText = value;
        RtfHelpers.AddMissingStyles(richEditControl1);
    });
    

    无需将控件作为参数传递给委托。 C# 自动创建一个closure

    如果你必须返回一个值,你可以使用这个实现:

    private static T InvokeIfRequiredReturn<T>(this Control control, Func<T> function)
    {
        if (control.InvokeRequired) {
            return (T)control.Invoke(function);
        } else {
            return function();
        }
    }
    

    更新

    根据其他几张海报Control可以概括为ISynchronizeInvoke

    public static void InvokeIfRequired(this ISynchronizeInvoke obj,
                                             MethodInvoker action)
    {
        if (obj.InvokeRequired) {
            var args = new object[0];
            obj.Invoke(action, args);
        } else {
            action();
        }
    }
    

    DonBoitnott 指出,与Control 不同,ISynchronizeInvoke 接口需要Invoke 方法的对象数组作为action 的参数列表。


    更新 2

    Mike de Klerk 建议的编辑(请参阅第一个代码 sn-p 中的注释以获取插入点):

    // When the form, thus the control, isn't visible yet, InvokeRequired  returns false,
    // resulting still in a cross-thread exception.
    while (!control.Visible)
    {
        System.Threading.Thread.Sleep(50);
    }
    

    有关此建议的疑虑,请参阅下面的 ToolmakerSteve'snawfal's cmets。

    【讨论】:

    • ISynchronizeInvoke 代替Control 不是更好吗? (感谢 Jon Skeet stackoverflow.com/questions/711408/…
    • @mike-de-clerk,我很担心你建议添加while (!control.Visible) ..sleep..。对我来说,这有一种不好的代码气味,因为它可能是一个潜在的无限延迟(在某些情况下甚至可能是无限循环),在可能有调用者不期望这种延迟(甚至是死锁)的代码中。恕我直言,Sleep 的任何使用都应该是每个调用者的责任,或者应该在一个单独的包装中,清楚地标明其后果。恕我直言,通常最好“失败”(例外,在测试期间捕获),或者如果控件没有准备好“什么也不做”。评论?
    • while (!Visible) 需要超时。可能导致难以调试的无限循环的不良做法。
    • 它没有,因为无论如何我们都在调用 winforms 控件。另见docs.microsoft.com/en-us/dotnet/api/…
    • @OlivierJacot-Descombes,如果您能解释一下 thread.invokerequired 背后的工作原理,那就太好了?
    【解决方案5】:

    我宁愿使用方法 Delegate 的单个实例,而不是每次都创建一个新实例。 在我的情况下,我曾经显示来自 Backroundworker 从 sql 实例复制和转换大数据的进度和(信息/错误)消息。在大约 70000 次进度和消息调用之后,我的表格每隔一段时间就会停止工作并显示新消息。 当我开始使用单个全局实例委托时,这并没有发生。

    delegate void ShowMessageCallback(string message);
    
    private void Form1_Load(object sender, EventArgs e)
    {
        ShowMessageCallback showMessageDelegate = new ShowMessageCallback(ShowMessage);
    }
    
    private void ShowMessage(string message)
    {
        if (this.InvokeRequired)
            this.Invoke(showMessageDelegate, message);
        else
            labelMessage.Text = message;           
    }
    
    void Message_OnMessage(object sender, Utilities.Message.MessageEventArgs e)
    {
        ShowMessage(e.Message);
    }
    

    【讨论】:

      【解决方案6】:

      您永远不应该编写如下所示的代码:

      private void DoGUISwitch() {
          if (object1.InvokeRequired) {
              object1.Invoke(new MethodInvoker(() => { DoGUISwitch(); }));
          } else {
              object1.Visible = true;
              object2.Visible = false;
          }
      }
      

      如果您确实有类似这样的代码,那么您的应用程序就不是线程安全的。这意味着您的代码已经从不同的线程调用 DoGUISwitch()。现在检查它是否在不同的线程中为时已晚。在调用 DoGUISwitch 之前,必须调用 InvokeRequire。您不应从其他线程访问任何方法或属性。

      参考:Control.InvokeRequired Property 您可以在其中阅读以下内容:

      除了 InvokeRequired 属性外,还有四种方法 一个可以线程安全调用的控件:Invoke、BeginInvoke、EndInvoke 和 CreateGraphics 如果控件的句柄已经 已创建。

      在单 CPU 架构中没有问题,但在多 CPU 架构中,您可以将部分 UI 线程分配给正在运行调用代码的处理器...如果该处理器不同于UI 线程正在运行,然后当调用线程结束时,Windows 会认为 UI 线程已经结束并将终止应用程序进程,即您的应用程序将退出而不会出错。

      【讨论】:

      • 您好,谢谢您的回答。自从我问这个问题以来已经有好几年了(自从我使用 C# 以来几乎一样长的时间),但我想知道你是否可以进一步解释一下?您链接到的文档提到了在控件被赋予句柄之前调用 invoke() 等的特定危险,但恕我直言并没有描述您所描述的内容。所有这些invoke() 废话的全部意义在于以线程安全的方式更新 UI,我认为将 more 指令放在阻塞上下文中会导致口吃? (呃……很高兴我停止使用 M$ 技术。太复杂了!)
      • 我还想指出,尽管经常使用原始代码(很久以前),但我在我的双 CPU 桌面上没有观察到您描述的问题
      • 我怀疑这个答案是否准确,因为 MSDN 显示了大量的例子,就像 OP 给出的那样。
      【解决方案7】:

      这是 Lee、Oliver 和 Stephan 答案的改进/组合版本。

      public delegate void InvokeIfRequiredDelegate<T>(T obj)
          where T : ISynchronizeInvoke;
      
      public static void InvokeIfRequired<T>(this T obj, InvokeIfRequiredDelegate<T> action)
          where T : ISynchronizeInvoke
      {
          if (obj.InvokeRequired)
          {
              obj.Invoke(action, new object[] { obj });
          }
          else
          {
              action(obj);
          }
      } 
      

      该模板允许灵活且无强制转换的代码更具可读性,而专用委托提供效率。

      progressBar1.InvokeIfRequired(o => 
      {
          o.Style = ProgressBarStyle.Marquee;
          o.MarqueeAnimationSpeed = 40;
      });
      

      【讨论】:

        【解决方案8】:

        我有点喜欢做一些不同的事情,如果需要,我喜欢用一个动作来称呼“我自己”,

            private void AddRowToListView(ScannerRow row, bool suspend)
            {
                if (IsFormClosing)
                    return;
        
                if (this.InvokeRequired)
                {
                    var A = new Action(() => AddRowToListView(row, suspend));
                    this.Invoke(A);
                    return;
                }
                 //as of here the Code is thread-safe
        

        这是一个方便的模式,IsFormClosing 是我在关闭表单时设置为 True 的字段,因为可能有一些后台线程仍在运行...

        【讨论】:

          【解决方案9】:

          用法:

          control.InvokeIfRequired(c => c.Visible = false);
          
          return control.InvokeIfRequired(c => {
              c.Visible = value
          
              return c.Visible;
          });
          

          代码:

          using System;
          using System.ComponentModel;
          
          namespace Extensions
          {
              public static class SynchronizeInvokeExtensions
              {
                  public static void InvokeIfRequired<T>(this T obj, Action<T> action)
                      where T : ISynchronizeInvoke
                  {
                      if (obj.InvokeRequired)
                      {
                          obj.Invoke(action, new object[] { obj });
                      }
                      else
                      {
                          action(obj);
                      }
                  }
          
                  public static TOut InvokeIfRequired<TIn, TOut>(this TIn obj, Func<TIn, TOut> func) 
                      where TIn : ISynchronizeInvoke
                  {
                      return obj.InvokeRequired
                          ? (TOut)obj.Invoke(func, new object[] { obj })
                          : func(obj);
                  }
              }
          }
          

          【讨论】:

            猜你喜欢
            • 2012-03-13
            • 2021-08-23
            • 1970-01-01
            • 1970-01-01
            • 2011-07-30
            • 2014-02-16
            • 2011-03-26
            • 2016-08-20
            • 1970-01-01
            相关资源
            最近更新 更多