多线程辅助类,可在线程忙时访问其属性和方法

© Conmajia 2012-2023
首发:2012.08.05 00:44:45
Miscellaneous Series: 128.1

多线程(multithreading)是一种充分利用中央处理器运行切片以提高程序运行效率和性能的常用技术。通常,高性能应用采取前后台方式,即将高负荷计算交由后台线程计算,而前台线程——一般是图形用户界面(GUI)——负责刷新显示计算结果。

在 .NET Framework 中,出于线程安全考虑,默认设定下不允许跨线程访问 GUI 属性和方法。跨线程的访问均会触发例如图 2 所示的 InvalidOperationException 无效操作异常。

多线程辅助类,可在线程忙时访问其属性和方法_第1张图片 图 2. 跨线程访问时引起了 `InvalidOperationException` 异常

尽管可以通过设置 Control.CheckForIllegalCrossThreadCallsfalse 禁用此检查实现跨线程访问,但这将带来严重的线程安全隐患。

以 GUI 线程为例,更安全的跨线程访问技巧通常利用 Control.InvokeRequired 属性和 Control.Invoke 方法实现。典型的代码如下。

public void DoWork() {
	if (control1.InvokeRequired) {
		control1.Invoke(DoWork);
	} else {
		// work code
	}
}

为了便于使用,以下给出一个名为 InvokeHelper 的多线程辅助类,用于较为简洁地实现跨线程访问主线程属性、方法。

InvokeHelper 有效代码约 150 行,主要方法为:

  1. InvokeHelper.Invoke
    调用主线程某个方法并返回该方法执行结果。
InvokeHelper.Invoke(<控件>, "<方法名>", <参数>);
  1. InvokeHelper.Get
    返回主线程某个属性值。
InvokeHelper.Get(<控件>, "<属性名>");
  1. 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 线程不受影响且仍可访问该线程属性。

多线程辅助类,可在线程忙时访问其属性和方法_第2张图片 图 1. InvokeHelper 访问处于阻塞情况下的线程属性和方法

其中工作线程运行的阻塞代码如下。

// 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

你可能感兴趣的:(C#,控件,算法,开发语言,c#)