1. 跨线程访问出现错误例子:
点击“测试”,创建一个线程,从0循环到10000给文本框赋值,代码如下:
private void Button1_Click(object sender, EventArgs e)
{
//创建一个线程去执行这个方法:创建的线程默认是前台线程
Thread thread = new Thread(new ThreadStart(Test));
//将线程设置为后台线程
thread.IsBackground = true;
//Start方法标记这个线程就绪了,可以随时被执行,具体什么时候行这个线程,由CPU决定
thread.Start();
}
private void Test()
{
for (int i = 0; i< 10000; i++)
{
this.textBox1.Text = i.ToString();
}
}
运行结果:
产生错误的原因:textBox1是由主线程创建的,thread线程是另外创建的一个线程,在.NET上执行的是托管代码,C#强制要求这些代码必须是线程安全的,即不允许跨线程访问Windows窗体的控件。
2. 解决方案: 使用回调函数
什么叫回调函数呢?比如,你调用了一个函数,那么就叫调用,但是如果你在调用一个函数的时候,还需要把一个函数提供该函数,让这个函数来调用你的函数,那么你提供的这个函数就被称为回调函数(callback)C#的方法回调函数(机制),也是建立在委托基础上的。
使用方法回调,实现给文本框赋值:
private delegate void setTextValueCallBack(int value);
//声明回调
private setTextValueCallBack setCallBack;
private void Button1_Click(object sender, EventArgs e)
{
//实例化回调
setCallBack = SetValue;
//创建一个线程去执行这个方法:创建的线程默认是前台线程
Thread thread = new Thread(Test);
//Start方法标记这个线程就绪了,可以随时被执行,具体什么时执这个线程,由CPU决定
//将线程设置为后台线程
thread.IsBackground = true;
thread.Start();
}
private void Test()
{
for (int i = 0; i < 10000; i++)
{
//textBox1.Invoke拥有此控件的基础窗口句柄的线程上执行指定的委托。
//使用回调。主线程执行setCallBack委托
//也就是说,Invoke()是一个方法,这方法执行的是委托,并且是在一个固定的线程上执行的。
//什么样的 线程? 拥有此控件的基础窗口句柄的线程。
// 这个“拥有此控件的基础窗口句柄的线程”实际上就是 “主线程”,
//窗体加载时 程序会创建一个 “主线程”。
if (textBox1.InvokeRequired)//如果为跨线程访问
{
textBox1.Invoke(setCallBack, i);//i就是传给回调函数的值
}
}
}
补充:Control.InvokeRequired
这个属性来进行判断是否是调用方对该控件进行调用控件,如果不是创建这个控件的线程来调用它,则返回true(即是跨线程访问就为true),否则返回False。
以上代码可以简化,只需要下面2个方法即可,利用内置委托Action,传入一个参数,内置委托即是回调函数,i是传给委托/回调函数的参数:
private void Test()
{
for (int i = 0; i < 10000; i++)
{
if (textBox1.InvokeRequired)
{
textBox1.Invoke(new Action<int>(n => { this.textBox1.Text = n.ToString(); }),i);
}
}
}
private void Button1_Click(object sender, EventArgs e)
{
Thread th = new Thread(Test);
th.IsBackground = true;
th.Start();
}
从以上回调实现的一般过程可知:C#的回调机制,实质上是委托的一种应用。
本文根据徐照兴教授讲义编写,有些改动。