65.9K
CodeProject 正在变化。 阅读更多。
Home

避免 InvokeRequired

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (135投票s)

2009年6月25日

CPOL

12分钟阅读

viewsIcon

524170

downloadIcon

3244

如何避免询问 InvokeRequired,实现最少的代码且无复制粘贴

第二版简介

这是我最初文章的一个新的、扩展的、改进的版本。这(并且仍然是)我第一篇(也是目前为止唯一一篇)CodeProject 文章。它获得了好评,但同时,很多人提供了修正、替代方案和改进。所以,我将利用所有这些新信息向您展示一种更好的实现相同事情的方法。我的代码中的每一个改进都是由别人建议的,所以这是一种集体写作。我只是在整理论坛上可以读到的内容。但由于我并不总是阅读论坛,所以对我来说,做一些“面向文本的维护”是有意义的。我会尽量给每个人应得的荣誉。

下载中现在有两个解决方案,一个适用于 VS2008,包含四个项目(C# 3.5 和 C# 2.0 的桌面和紧凑型框架),另一个适用于 VS2005,包含四个项目(C# 2.0 的桌面和紧凑型框架,每种框架有两种不同的解决问题的方法)。

在文章的最后,您将找到我认为现在是解决此问题的“官方”方法(Microsoft 的方法)。直到几个人在论坛上发布它,我才得知此方法。至少阅读一下 (点击这里)。但我将坚持我解决此问题的方式。接下来的文本几乎与原始文章完全相同;新文本从第二条水平线下方开始。请享用!

引言

正如您应该已经知道的,当您需要从多个线程访问用户界面时,使用 Windows.Forms 会变得非常糟糕。在我看来,这是 “泄露的抽象” 的一个例子。我不知道(也不想知道)为什么我不能简单地写:

this.text = "New Text";

在任何线程中,Windows.Forms.Control 类都应该为我抽象任何线程问题。但它并没有。我将尝试展示解决此问题的几种方法,然后是我想到的最简单的解决方案。请等到最后阅读精彩内容 (或点击这里)

值得一提的一点是:当您在 Visual Studio 中运行一个存在此 UI 线程问题的程序时,它将 **始终** 抛出异常。同一个程序作为独立的 EXE 运行时可能不会抛出异常。也就是说,开发环境比 .NET Framework 更严格。这是一件 **好事**,在开发时解决问题总是比在生产环境中出现随机问题要好。

这是我的第一篇文章,英语不是我的母语,所以请温和一些!

“标准”模式

我不知道最初是谁提出了这段代码,但这是解决线程问题的标准解决方案

public delegate void DelegateStandardPattern();
private void SetTextStandardPattern()
{
    if (this.InvokeRequired)
    {
        this.Invoke(new DelegateStandardPattern(SetTextStandardPattern));
        return;
    }
    this.text = "New Text";
}

此解决方案的优点

  • 它能完成工作。
  • 它适用于 C# 1.0、2.0、3.0、3.5、Standard 和 Compact Framework(自 CF 1.1 起,CF 1.0 中没有 InvokeRequired)。
  • 每个人都在使用它,所以当你看到这样的代码时,你就知道这段代码很可能将从另一个线程调用。

缺点

  • 更新一个文本需要很多代码!
  • 你需要复制粘贴它,你无法从中创建一个通用方法。
  • 如果你需要调用一个带有参数的方法,你甚至无法重用委托。你需要为每个不同的参数集声明另一个委托。
  • 它很丑。我知道这是主观的,但确实如此。我尤其讨厌需要在方法“外部”声明委托。

有一些巧妙的解决方案,例如 使用 AOP 的这个,以及 使用反射的这个。但我想要一个更容易实现的。一种方法是使用 SurroundWith 代码片段,但我希望我的代码问题由语言而不是 IDE 来解决。而且,它只会解决复制粘贴问题,但对于非常简单的事情来说,它仍然需要大量代码。

为什么我们不能通用化标准模式?因为在 .NET 1.0 中无法将代码块作为参数传递,因为 C# 最初几乎不支持函数式编程风格。

“匿名委托”模式

随着 C# 2.0 的出现,我们有了匿名委托和 MethodInvoker 类,因此我们可以将标准模式简化为这样

private void SetTextAnonymousDelegatePattern()
{
    if (this.InvokeRequired)
    {
        MethodInvoker del = delegate { SetTextAnonymousDelegatePattern(); };
        this.Invoke(del);
        return;
    }
    this.text = "New Text";
}

这是一个稍微好一点的解决方案,但我从未见过有人使用它。但是,如果我们不是执行 this.text = "New Text"; 而是需要调用一个带有参数的方法怎么办?例如

private void MultiParams(string text, int number, DateTime dateTime);

这没什么大不了的,因为委托可以访问外部变量。所以,您可以这样写:

private void SetTextDelegatePatternParams(string text, int number, DateTime datetime)
{
    if (this.InvokeRequired)
    {
        MethodInvoker del = delegate { 
		SetTextDelegatePatternParams(text, number, datetime); };
        this.Invoke(del);
        return;
    }
    MultiParams(text, number, datetime);
}

如果你“忘记”询问是否需要调用 Invoke,则“匿名委托”模式可以大大简化。这就引出了...

“匿名委托最小化”模式

这真的很棒

//No parameters
private void SetTextAnonymousDelegateMiniPattern()
{
    Invoke(new MethodInvoker(delegate
    {
    	this.text = "New Text";
    }));
}
//With parameters
private void SetTextAnonymousDelegateMiniPatternParams
		(string text, int number, DateTime dateTime)
{
    Invoke(new MethodInvoker(delegate
    {
    	MultiParams(text, number, dateTime);
    }));
}

它有效,易于编写,离完美只有几行之遥。我第一次看到它时,以为这就是我想要的。那么问题是什么呢?嗯,我们忘记询问是否需要 Invoke。而且,由于这不是标准做法,所以别人(或者几个月后的我们自己)不会清楚为什么我们要这样做。我们可以做得好一点,给代码加上注释,但说实话,我们都知道自己不会。至少我更喜欢我的代码更具“意图揭示性”。所以,我们有了...

“UIThread”模式,或者我解决此问题的方式

首先,我给您展示“兔子”

//No parameters
private void SetTextUsingPattern()
{
    this.UIThread(delegate
    {
    	this.text = "New Text";
    });
}
//With parameters
private void SetTextUsingPatternParams(string text, int number, DateTime dateTime)
{
    this.UIThread(delegate
    {
    	MultiParams(text, number, dateTime);
    });
}

现在,我来给您展示“技巧”。这是一个简单的 static 类,只有一个方法。当然,它是一个扩展方法,所以如果您有“扩展方法不是纯粹面向对象编程”之类的异议,我建议您使用 Smalltalk,停止抱怨。或者使用标准的辅助类,随您便。不包含注释、命名空间和 using,该类看起来如下:

static class FormExtensions
{
    static public void UIThread(this Form form, MethodInvoker code)
    {
        if (form.InvokeRequired)
        {
            form.Invoke(code);
            return;
        }
        code.Invoke();
    }
}

这就是我独自走到的地步。但随后,我收到了来自论坛开发者的以下建议:

  • was333 说:为什么只针对 Form?为什么不是 Control?他说得对。甚至还有一个更抽象的接口(ISynchronizeInvoke),Rob Smiley 建议使用,但我认为它太奇怪了,并且在 Compact Framework 中不存在。
  • Borlip 指出 MethodInvoker 不存在于 CompactFramework 中,但 Action 存在,因此使用 Action 更具可移植性。
  • tzach shabtay 链接到 这篇文章,指出在可能的情况下使用 BeginInvoke 而不是 Invoke 更好。有时这可能会成为一个问题,所以我们需要两个版本。但您应该优先使用 BeginInvoke

所以,到目前为止,这是最终版本

static class ControlExtensions
{
    static public void UIThread(this Control control, Action code)
    {
        if (control.InvokeRequired)
        {
            control.BeginInvoke(code);
            return;
        }
        code.Invoke();
    }
	
    static public void UIThreadInvoke(this Control control, Action code)
    {
        if (control.InvokeRequired)
        {
            control.Invoke(code);
            return;
        }
        code.Invoke();
    }
}

您可以使用它,如下所示:

this.UIThread(delegate
{
   textBoxOut.Text = "UIThread pattern was used";
});

如您所见,它只是标准模式,尽可能地通用化。此解决方案的优点:

  • 它能完成工作。
  • 它在 Full Framework 和 Compact Framework 中工作方式相同。
  • 它很简单(几乎看起来像一个 using{} 块!)。
  • 它不关心您是否有参数。
  • 如果您三个月后再次阅读它,它仍然会看起来清晰易懂。
  • 它利用了现代 .NET 的许多优势:匿名委托、扩展方法、 lambda 表达式(如果您愿意,稍后会看到)。

缺点

  • 呃……等待您的评论。再次。

关注点

您可以使用 lambda 风格写出更少的代码,如果您只需要写一行,您可以做到像这样简单:

private void SetTextUsingPatternParams(string text, int number, DateTime dateTime)
{
    this.UIThread(()=> MultiParams(text, number, dateTime));
}

而且仍然清晰!如果您需要从 Form 读取,您需要使用 UIThreadInvoke,否则会发现奇怪的结果。

private void Read()
{
     string textReaded;
     this.UIThreadInvoke(delegate
     {
        textReaded = this.Text;
     });
}

但我很确定,如果您从另一个线程读取屏幕,那么您在某个地方就犯了错误。

对于 C# 2.0 和 Visual Studio 2008

此代码需要 .NET Framework 3.5 才能工作。它开箱即用,支持桌面和紧凑型框架。在可下载的代码中,您有一个适用于两者的工作示例。有些人询问了 .NET 2.0 版本。.NET 3.5 中有两样东西是我们 2.0 所缺乏的:

  1. Action 类:我们有 Action<T>,但没有简单的无参数类型 Action,因为 ActionSystem.Core.dll 中。这很简单,我们只需在 System 命名空间内创建委托:
namespace System
{
    public delegate void Action();
}
  1. 扩展方法:感谢 Kwan Fu Sit 指出了 这篇文章,有一个巧妙的方法可以做到这一点,如果您可以使用 Visual Studio 2008。由于扩展方法只是一个编译器技巧,您需要添加到项目中的唯一内容是一个新类:
namespace System.Runtime.CompilerServices
{
    [AttributeUsage
    (AttributeTargets.Method|AttributeTargets.Class|AttributeTargets.Assembly)]
    public sealed class ExtensionAttribute : Attribute
    {

    }
}

就是这样!它非常有用,不仅限于此 UIThread 技巧。我在同一个文件 CSharp35Extras.cs 中添加了 ExtensionAttributeAction。请在同一个解决方案的相关项目中查看详细信息。再次,完全相同的代码适用于桌面和紧凑型框架。

对于 C# 2.0 和 Visual Studio 2005

我发现基本上有三种方法可以在 VS2005 中实现,而且它们都不是非常优雅。在所有这些方法中,我都使用 MethodInvoker 而不是 Action,因为 MethodInvoker 存在于桌面 .NET Framework 2.0 中。如果您在 Compact Framework 中工作,仍然需要在某处声明 MethodInvoker 类。对于简单的单窗口项目(或者更准确地说,是具有多线程问题的单窗口项目),只需将方法复制到 FormWathever.cs 中即可。

private void UIThread(MethodInvoker code)
{
    if (this.InvokeRequired)
    {
        this.BeginInvoke(code);
        return;
    }
    this.Invoke();
}

您可以这样使用它:

UIThread(delegate
{
   textBoxOut.Text = "UIThread pattern was used";
});

我认为对于简单的项目来说,这已经足够好了。但当您在第二个窗口中遇到相同的问题并开始复制粘贴该方法时,那就不是那么好了。

另一种选择是创建一个如下所示的辅助类:

static public class UIHelper
{
    static public void UIThread(Control control, MethodInvoker code)
    {
        if (control.InvokeRequired)
        {
            control.BeginInvoke(code);
            return;
        }
        control.Invoke();
    }	
}

然后像这样调用 UIThread

UIHelper.UIThread(this, delegate
{
   textBoxOut.Text = "New text";
});

我没意见有一个 UIHelper 类,出于各种原因我总是最终使用 UIHelper 类,但我不喜欢 UIHelper.UIThread(this,... 部分。对我来说,这太啰嗦了。但它有效,而且至少您没有复制粘贴代码。

另一种方法是创建一个 FormBase 类,如下所示:

public class FormBase : Form
{
   public void UIThread(MethodInvoker code)
   {
       if (this.InvokeRequired)
       {
           this.BeginInvoke(code);
           return;
       }
       code.Invoke();
   }
}

然后 **让您的所有窗体都继承自 FormBase**,然后像这样调用:

UIThread(delegate
{
   textBoxOut.Text = "New text";
});

调用部分还可以,但我并不喜欢让我的所有窗体都继承自 FormBase,特别是当有时我使用视觉继承,并且切换到设计模式时,Visual Studio 会向我显示一些非常糟糕的屏幕,例如这个:

(关于这个问题,我所知道的唯一解决方法是关闭所有设计选项卡,然后生成-清除解决方案,然后关闭 Visual Studio,然后删除 binobj 文件夹下的所有文件,重新打开 Visual Studio,重新生成解决方案,然后再次在设计视图中打开 FormWhatever)。

您还失去了 Control 的泛化;这种方法只适用于 Form。您可以选择其中一种部分解决方案,迁移到 VS2008,或者给您的老板施压迁移到 VS2008。来吧!VS2010 就在眼前。

对于 C# 1.0 和 Visual Studio 2003

你在开玩笑吗?(我的意思是,我没有那个环境的解决方案,而且我认为这是不可能的。)

替代方案

在我写这篇文章的第一版时,我知道一些避免复制粘贴代码的替代方法。我都不喜欢,这就是我的动机,但我在文章开头列出了这些替代方法,以便展示给大家。然而,还有另一种我没想到的替代方法:Alomgir Miah A 建议使用 BackgroundWorker。我认为它太啰嗦了,而且它不存在于 Compact Framework 中。但当然,它存在于完整框架中,并且可以用来避免线程问题。

两个人建议使用 Control.CheckForIllegalCrossThreadCalls = false;。**请勿这样做!** 这是极其错误的!根据 MSDN 文档:“当应用程序在调试器外部启动时,非法的跨线程调用将始终引发异常。”设置 Control.CheckForIllegalCrossThreadCalls = false; 只会禁用调试器检测所有可能的线程问题的能力。所以,您可能在调试时检测不到它们,但当您的应用程序运行时,您可能会遇到可怕的异常导致应用程序崩溃,并且您永远无法重现它们。您选择在过马路时闭上眼睛。这样做很容易,但风险很高。再次强调,**请勿这样做!** 使用任何适合您的解决方案,永远不要编写 Control.CheckForIllegalCrossThreadCalls = false;

“官方”模式

最后,另外两个人(Islam ElDemery 和 Member 170334,他没有名字也没有友好的 URL)向我展示了我认为 Microsoft 为此问题开发的官方解决方案,因此它可能比我的更好:SynchronizationContext 类。我不得不承认我不知道它,而且它自 .NET Framework 2.0 起就可用了!它可以非常像我自己的解决方案一样使用,并且可能更快,因为它包含在框架中,并且提供了更多的选项。我足够成熟,可以在这里展示这个解决方案,即使它让我的工作变得毫无用处,我也足够幼稚,可以稍后拒绝它。这是一个两步解决方案:首先,您需要一个 SynchronizationContext 成员,该成员必须在构造函数中初始化:

class FormWathever
{
    private SynchronizationContext synchronizationContext ;
	
	public FormWathever()
	{
	    this.synchronizationContext  = SynchronizationContext.Current;
		//the rest of your code
	}
}

然后,当您需要执行一些对线程不安全的窗体更新时,您应该使用类似以下的方法:

synchronizationContext.Send(new SendOrPostCallback( 
    delegate(object state) 
    {    
        textBoxOut.Text = "New text";
    } 
), null);

它有效,它已集成到框架中,我相信它很快,并且有一些额外的选项。为什么不使用它?我只能想到四个原因,而且不是很好的原因。它们看起来更像是借口。

  1. 我不喜欢初始化部分。我不明白为什么 Microsoft 没有在 Form 类中包含一个 SynchronizationContext 属性,并在基构造函数中自动初始化它。为了跳过自己进行初始化,您需要让所有窗体都继承自 FormBase 或类似的类。
  2. 它有点啰嗦。您需要创建那个 SendOrPostCallback 对象,并传递那个额外的 null 参数,以及那个额外的 object 状态。您可以通过使用另一个辅助方法来避免这项额外工作,但在这种情况下,我将坚持使用 UIThread
  3. 它不是“意图揭示的代码”。而且由于它不是很流行,它会使您的代码更难被他人理解和维护。(但也不是太多,说实话。)
  4. 它不存在于 Compact Framework 中。

但如果您不必关心 Compact Framework,并且认为多输入几个字符不会要您命,那么这很可能是您应该选择的路径。

历史

  • 2009年6月24日:初始版本
  • 2009年8月29日:扩展版,多项改进,支持 C# 2.0,支持 VS2005,以及“官方”模式
© . All rights reserved.