点击“测试”,创建一个线程,从0循环到10000给文本框赋值,代码如下:
private void btn_Test_Click(object sender, EventArgs e) { //创建一个线程去执行这个方法:创建的线程默认是前台线程 Thread thread = new Thread(new ThreadStart(Test)); //Start方法标记这个线程就绪了,可以随时被执行,具体什么时候执行这个线程,由CPU决定 //将线程设置为后台线程 thread.IsBackground = true; thread.Start(); } private void Test() { for (int i = 0; i < 10000; i++) { this.textBox1.Text = i.ToString(); } }
运行结果:
产生错误的原因:textBox1是由主线程创建的,thread线程是另外创建的一个线程,在.NET上执行的是托管代码,C#强制要求这些代码必须是线程安全的,即不允许跨线程访问Windows窗体的控件。
解决方案:
1、在窗体的加载事件中,将C#内置控件(Control)类的CheckForIllegalCrossThreadCalls属性设置为false,屏蔽掉C#编译器对跨线程调用的检查。
private void Form1_Load(object sender, EventArgs e) { //取消跨线程的访问 Control.CheckForIllegalCrossThreadCalls = false; }
使用上述的方法虽然可以保证程序正常运行并实现应用的功能,但是在实际的软件开发中,做如此设置是不安全的(不符合.NET的安全规范),在产品软件的开发中,此类情况是不允许的。如果要在遵守.NET安全标准的前提下,实现从一个线程成功地访问另一个线程创建的空间,要使用C#的方法回调机制。
2、使用回调函数
回调实现的一般过程:
C#的方法回调机制,也是建立在委托基础上的,下面给出它的典型实现过程。
(1)、定义、声明回调。
1 //定义回调 2 private delegate void DoSomeCallBack(Type para); 3 //声明回调 4 DoSomeCallBack doSomaCallBack;
可以看出,这里定义声明的“回调”(doSomaCallBack)其实就是一个委托。
(2)、初始化回调方法。
doSomeCallBack=new DoSomeCallBack(DoSomeMethod);
所谓“初始化回调方法”实际上就是实例化刚刚定义了的委托,这里作为参数的DoSomeMethod称为“回调方法”,它封装了对另一个线程中目标对象(窗体控件或其他类)的操作代码。
(3)、触发对象动作
Opt obj.Invoke(doSomeCallBack,arg);
其中Opt obj为目标操作对象,在此假设它是某控件,故调用其Invoke方法。Invoke方法签名为:
object Control.Invoke(Delegate method,params object[] args);
它的第一个参数为委托类型,可见“触发对象动作”的本质,就是把委托doSomeCallBack作为参数传递给控件的Invoke方法,这与委托的使用方式是一模一样的。
最终作用于对象Opt obj的代码是置于回调方法体DoSomeMethod()中的,如下所示:
private void DoSomeMethod(type para)
{
//方法体
Opt obj.someMethod(para);
}
如果不用回调,而是直接在程序中使用“Opt obj.someMethod(para);”,则当对象Opt obj不在本线程(跨线程访问)时就会发生上面所示的错误。
从以上回调实现的一般过程可知:C#的回调机制,实质上是委托的一种应用。在C#网络编程中,回调的应用是非常普遍的,有了方法回调,就可以在.NET上写出线程安全的代码了。
使用方法回调,实现给文本框赋值:
namespace MultiThreadDemo { public partial class Form1 : Form { public Form1() { InitializeComponent(); } //定义回调 private delegate void setTextValueCallBack(int value); //声明回调 private setTextValueCallBack setCallBack; private void btn_Test_Click(object sender, EventArgs e) { //实例化回调 setCallBack = new setTextValueCallBack(SetValue); //创建一个线程去执行这个方法:创建的线程默认是前台线程 Thread thread = new Thread(new ThreadStart(Test)); //Start方法标记这个线程就绪了,可以随时被执行,具体什么时候执行这个线程,由CPU决定 //将线程设置为后台线程 thread.IsBackground = true; thread.Start(); } private void Test() { for (int i = 0; i < 10000; i++) { //使用回调 textBox1.Invoke(setCallBack, i); } } /// <summary> /// 定义回调使用的方法 /// </summary> /// <param name="value"></param> private void SetValue(int value) { this.textBox1.Text = value.ToString(); } } }