? Conmajia 2012-2023
首发:2012.08.05 00:44:45
Miscellaneous Series: 128.1
多线程(multithreading)是一种充分利用中央处理器运行切片以提高程序运行效率和性能的常用技术。通常,高性能应用采取前后台方式,即将高负荷计算交由后台线程计算,而前台线程——一般是图形用户界面(GUI)——负责刷新显示计算结果。
在 .NET Framework 中,出于线程安全考虑,默认设定下不允许跨线程访问 GUI 属性和方法。跨线程的访问均会触发例如图 2 所示的 InvalidOperationException
无效操作异常。
尽管可以通过设置 Control.CheckForIllegalCrossThreadCalls
为 false
禁用此检查实现跨线程访问,但这将带来严重的线程安全隐患。
以 GUI 线程为例,更安全的跨线程访问技巧通常利用 Control.InvokeRequired
属性和 Control.Invoke
方法实现。典型的代码如下。
public void DoWork() {
if (control1.InvokeRequired) {
control1.Invoke(DoWork);
} else {
// work code
}
}
为了便于使用,以下给出一个名为 InvokeHelper
的多线程辅助类,用于较为简洁地实现跨线程访问主线程属性、方法。
InvokeHelper
有效代码约 150 行,主要方法为:
InvokeHelper.Invoke
InvokeHelper.Invoke(<控件>, "<方法名>", <参数>);
InvokeHelper.Get
InvokeHelper.Get(<控件>, "<属性名>");
InvokeHelper.Set
InvokeHelper.Set(<控件>, "<属性名>", <属性值>);
完整的 InvokeHelper
类实现源代码如下。
/*******************************************************************************
* InvokeHelper.cs
* A thread-safe control invoker helper class.
* -----------------------------------------------------------------------------
* Project:Conmajia.Controls
* Author:Conmajia
* History:
* 4th Aug., 2012
* Added support for "Non-control" controls (such as ToolStripItem).
* 4th Aug., 2012
* Initiated.
******************************************************************************/
// A thread-safe control invoker helper class.
public class InvokeHelper {
private delegate object MethodInvoker(
Control control, string methodName, params object[] args);
private delegate object PropertyGetInvoker(
Control control, object noncontrol, string propertyName);
private delegate void PropertySetInvoker(
Control control, object noncontrol, string propertyName, object value);
private static PropertyInfo GetPropertyInfo(
Control control, object noncontrol, string propertyName) {
if (control != null && !string.IsNullOrEmpty(propertyName)) {
PropertyInfo pi = null;
Type t = null;
if (noncontrol != null)
t = noncontrol.GetType();
else
t = control.GetType();
pi = t.GetProperty(propertyName);
if (pi == null)
throw new InvalidOperationException(
string.Format(
"Can't find property {0} in {1}.",
propertyName, t.ToString()));
return pi;
} else
throw new ArgumentNullException("Invalid argument.");
}
// Invoke a method
public static object Invoke(Control control, string methodName, params object[] args) {
if (control != null && !string.IsNullOrEmpty(methodName))
if (control.InvokeRequired)
return control.Invoke(
new MethodInvoker(Invoke),
control, methodName, args);
else {
MethodInfo mi = null;
if (args != null && args.Length > 0) {
Type[] types = new Type[args.Length];
for (int i = 0; i < args.Length; i++)
if (args[i] != null)
types[i] = args[i].GetType();
mi = control.GetType().GetMethod(methodName, types);
} else
mi = control.GetType().GetMethod(methodName);
// check method info you get
if (mi != null)
return mi.Invoke(control, args);
else
throw new InvalidOperationException("Invalid method.");
}
else
throw new ArgumentNullException("Invalid argument.");
}
// Get the value of a property
public static object Get(Control control, string propertyName) {
return Get(control, null, propertyName);
}
public static object Get(Control control, object noncontrol, string propertyName) {
if (control != null && !string.IsNullOrEmpty(propertyName))
if (control.InvokeRequired)
return control.Invoke(new PropertyGetInvoker(Get),
control,
noncontrol,
propertyName
);
else {
PropertyInfo pi = GetPropertyInfo(control, noncontrol, propertyName);
object invokee = (noncontrol == null) ? control : noncontrol;
if (pi != null)
if (pi.CanRead)
return pi.GetValue(invokee, null);
else
throw new FieldAccessException(
string.Format(
"{0}.{1} is a write-only property.",
invokee.GetType().ToString(),
propertyName
));
return null;
}
else
throw new ArgumentNullException("Invalid argument.");
}
// Set the value of a property
public static void Set(Control control, string propertyName, object value) {
Set(control, null, propertyName, value);
}
public static void Set(Control control, object noncontrol, string propertyName, object value) {
if (control != null && !string.IsNullOrEmpty(propertyName))
if (control.InvokeRequired)
control.Invoke(new PropertySetInvoker(Set),
control,
noncontrol,
propertyName,
value
);
else {
PropertyInfo pi = GetPropertyInfo(control, noncontrol, propertyName);
object invokee = (noncontrol == null) ? control : noncontrol;
if (pi != null)
if (pi.CanWrite)
pi.SetValue(invokee, value, null);
else
throw new FieldAccessException(
string.Format(
"{0}.{1} is a read-only property.",
invokee.GetType().ToString(),
propertyName
));
}
else
throw new ArgumentNullException("Invalid argument.");
}
}
借助多线程辅助类 InvokeHelper,用户能够实现在某线程执行阻塞式指令无法响应其他指令情况下,GUI 线程仍可以访问其中的属性和方法以更新用户界面。图 1 动画演示了这一过程。尽管工作线程处于阻塞状态,但 GUI 线程不受影响且仍可访问该线程属性。
其中工作线程运行的阻塞代码如下。
// Blocking
int count = 0;
while(true) {
// ...
Thread.Sleep(1000);
count++;
}
参考文献
[1] Sergiu Josan, Making controls thread-safely, May 2009.
[2] vicoB, Extension of safeInvoke, July 2010.
? Conmajia 2012