多线程中的InvokeRequired

引子

如你所知,当你需要从多线程中访问用户界面是,使用Windows.Forms变得十分可恶。如我拙见,这有一个leaky abstraction的例子。我不知道,我也不想知道为什么不能这样简单的写:

this.text = "New Text";

在任何线程中,对于线程问题 Windows.Forms.Control class 应该是抽象的。但是它没有!我将展示这个问题的几种解决办法,最终是我找到的最简单的解决办法。Wait till the end to find the good stuff (or click here)!

有一件事需要清楚——当你在Visual Studio中运行一个带UI线程的程序时,总是会抛出一个异常。而同样的程序作为一个单独的EXE运行时可能不会抛出异常。也就是说,开发环境比 .NET framework更为严格。这其实是一件好事,在开发阶段就解决问题比在生产中出现不可控制的问题要好的多。

这是我在这(CodeProject)的第一篇文章,而且英语并不是我的母语,所以请大家轻点拍!

“标准”模式
我不知道是谁第一个写出这个代码的,但是这是上述线程问题的标准解决办法:
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 and Compact Framework(从CF 1.1开始,在CF 1.0中不是必须用InvokeRequired)。
  • 每个人都用它,所以当你读到和这类似的代码时,你知道这些代码大致可以在其他线程中去调用。

其缺点是:

  • 仅仅是更新一个Text却需要大量的代码!
  • 你需要去复制/粘贴这段代码,却不能用一个通用的方法去表示。
  • 如果你需要去调用一个带参数的方法,你不能重用这个委托(delegate)。不同的参数类型,你需要声明不同的委托(delegate)。
  • 这种办法很难看(ugly)。我知道这种感觉很主观,但是它确实是这样的。我特别厌烦在一个方法(method)的外面声明一个委托(delegate)。

其实有很多很聪明的解决办法,像this one using AOP,还有this one using Reflection。但是我想更简便的去实现它。一种办法是SurroundWith code snippet, 但是我希望我的代码是从程序语言上去解决他,而不是用IDE的方式。同样,它只是解决了复制/粘贴的问题(见上述缺点第二条),仍然需要很多代码去解决像这样简单的问题(缺点第一条)。

为什么我们不能推广这种标准模式呢?因为在.NET 1.0中没有办法将一段代码作为参数传递,因为当C#刚诞生时,几乎是不支持函数式的编程风格。

 

 

“匿名委托”模式

 

由于在C# 2.0 中我们有匿名委托和MethodInvoke类,所以可以简化标准模式为:

 

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);

 

这也不是什么麻烦,因为委托(delegate)能够访问外部变量,所以你能这样改写:

 

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是

 

否是必须的。并且因为这不是一个“标准”的解决办法,这对于其他人来说

 

(或者在数个月之后对于我们自己),为什么我们要这样做,这还显得不够清晰。我

 

们当然能够很nice的对待这些代码,并且加一些注释,但是老实说吧,我们不会这样去做。

 

至少我希望我的代码能够更清楚的“展现意图”,所以,我们有——

“UIThread”模式,或者问题的最终解决之道

 首先我像你展示“rabbit”:

//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);
    });
}
现在你将看到我的“trick”。这是一个简单的静态(static)类,只有一个方法。当然,
这是一个扩展方法(extension method),如果你对这有些异议,如“扩展方法不是纯
粹的面向对象编程”(”extension methods are not pure object orientated programming”),
我建议你去用Smalltalk,停止抱怨。或者用标准的帮助类(standard helper class),如你所愿。
除去注释,namespace和using,这个类如下所示:
static class FormExtensions
{
    static public void UIThread(this Form form, MethodInvoker code)
    {
        if (form.InvokeRequired)
        {
            form.Invoke(code);
            return;
        }
        code.Invoke();
    }
}

就像你所看到的,这就像标准模式,尽可能的一般化。

这种解决方法的优点:

  • 确实解决了问题。
  • 对于Full and Compact Framework(with just one extra line)一样好使。
  • 它很简单(就像一个using{}block!)。
  • 不需要关注方法有没有参数。
  • 如果你三个月后再来读,它仍然显得很清晰。
  • 它使用了很多现代.NET(modern .NET)所提供的东东:匿名委托,扩展方法,lambda表示式(如果你想,请看后面部分),泛型类型推理?(generic type inference)。

缺点:

  • 呃……还是你们来说吧,轻点拍啊

 

Points of Interest

这些代码需要完整的.NET Framework 3.5支持!如果在Compact Framework中使用,需要简单的声明一下MethodInvoker:

public delegate void MethodInvoker();
用lambda风格你能写更少的代码,如果你只想写一行代码的话,可以像这样……
private void SetTextUsingPatternParams(string text, int number, DateTime dateTime)
{
    this.UIThread(()=> MultiParams(text, number, dateTime));
}
……仍然很清晰!
下载的文件中是一个简单的执行程序,展示了一些错误代码,如失败的,“标准模式”的,
还有我已经说过的“UIThread模式”的,还有两个工程,一个是完整的.NET Framework
工程,另一个是Compact Framework工程。

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        System.Timers.Timer timer = new System.Timers.Timer();
        Thread thread;
        
        int count;
        bool f;

        public Form1()
        {
            InitializeComponent();
            timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);
            timer.Interval = 10;
            thread = new Thread(TestFunc);
        }

        public void SetValue(int value)
        {
            textBox1.Text = value.ToString();
        }

        public void TestFunc()
        {
            while (f)
            {
                if (textBox1.InvokeRequired)
                {
                    //1.方法一
                    //MethodInvoker func = delegate { SetValue(count); };
                    //this.Invoke(func);

                    //2.方法二
                    Invoke(new MethodInvoker(delegate { textBox1.Text = count.ToString(); }));

                    //3.方法三
                    //Invoke(new MethodInvoker(delegate { SetValue(count); }));
                }
                else
                {
                    textBox1.Text = count.ToString();
                }
                count++;

                Thread.Sleep(1);
            }
        }

        void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
           // if (this.IsDisposed)  这个判断加了也没有效果
           //     return;

            if (textBox1.InvokeRequired)
            {
                //1.方法一
                //MethodInvoker func = delegate { SetValue(count); };
                //this.Invoke(func);

                //2.方法二
                Invoke(new MethodInvoker(delegate{textBox1.Text = count.ToString();}));

                //3.方法三
                //Invoke(new MethodInvoker(delegate { SetValue(count); }));
            }
            else
            {
                textBox1.Text = count.ToString();
            }
            count++;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            f = true;
            //timer.Start();
            thread.Start();
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            f = false;
            thread.Abort();

    //timer.Stop();

    //Application.DoEvents();  这一句非常关键,在定时器停止后,必须先让系统把剩余的定时消息处理掉,不然就会在定时处理函数中出现非法引用已经销毁的对象
        }
    }
}

posted on 2012-06-21 15:25  Arrow.Lu  阅读(1046)  评论(0)    收藏  举报