避免 InvokeRequired






4.83/5 (135投票s)
如何避免询问 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 所缺乏的:
Action
类:我们有Action<T>
,但没有简单的无参数类型Action
,因为Action
在 System.Core.dll 中。这很简单,我们只需在System
命名空间内创建委托:
namespace System
{
public delegate void Action();
}
- 扩展方法:感谢 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 中添加了 ExtensionAttribute
和 Action
。请在同一个解决方案的相关项目中查看详细信息。再次,完全相同的代码适用于桌面和紧凑型框架。
对于 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,然后删除 bin 和 obj 文件夹下的所有文件,重新打开 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);
它有效,它已集成到框架中,我相信它很快,并且有一些额外的选项。为什么不使用它?我只能想到四个原因,而且不是很好的原因。它们看起来更像是借口。
- 我不喜欢初始化部分。我不明白为什么 Microsoft 没有在
Form
类中包含一个SynchronizationContext
属性,并在基构造函数中自动初始化它。为了跳过自己进行初始化,您需要让所有窗体都继承自FormBase
或类似的类。 - 它有点啰嗦。您需要创建那个
SendOrPostCallback
对象,并传递那个额外的null
参数,以及那个额外的object
状态。您可以通过使用另一个辅助方法来避免这项额外工作,但在这种情况下,我将坚持使用UIThread
。 - 它不是“意图揭示的代码”。而且由于它不是很流行,它会使您的代码更难被他人理解和维护。(但也不是太多,说实话。)
- 它不存在于 Compact Framework 中。
但如果您不必关心 Compact Framework,并且认为多输入几个字符不会要您命,那么这很可能是您应该选择的路径。
历史
- 2009年6月24日:初始版本
- 2009年8月29日:扩展版,多项改进,支持 C# 2.0,支持 VS2005,以及“官方”模式