(此文章为小白本人学习过程的一些总结,大佬就可以略过拉,有点指点就更好了。)
创建一个“测试”,使其将1-10000给文本框赋值
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
//创建一个"测试",将1-10000给文本框赋值
private void button1_Click(object sender, EventArgs e)
{
//创建线程调用方法
Thread th = new Thread(Test);
th.IsBackground = true;//设置为后台线程
th.Start();
}
//定义一个方法,使其将0—10000之间的数循环打印在文本框里面
private void Test()
{
for (int i = 1; i <= 10000; i++)
{
textBox1.Text = i.ToString();
}
}
}
在创建好“测试”程序后,运行,点击测试,这时候就会弹出一个错误:
上面的错误信息显示:线程间操作无效:从不是创建控件“textBox1”的线程访问它。
产生错误的原因:textBox1是由主线程创建的,thread线程是另外创建的一个线程。在.NET上执行的是托管代码,C#强制要求这些代码必须是线程安全的,即不允许跨线程访问Windows窗体的控件。
解决方案:
方法一、在窗体加载事件中,将C#内置控件(Control)类的CheckForIllegalCrossThreadCalls属性设置为false,屏蔽C#编译器对跨线程调用的检查;
private void Form1_Load(object sender, EventArgs e)
{
//屏蔽窗体加载事件当中C#编译器对跨线程访问的检查
Control.CheckForIllegalCrossThreadCalls = false;
}
修改完了之后,运行测试,就能够把结果呈现出来。
使用上述的方法虽然能够让程序正常进行并且实现应用功能,但是在实际的软件开发当中,这样的设置是不安全的,是不符合.net的安全规范的,并且在软件开发当中这样的清空是不允许的。如果要在遵守.NET安全规范的情况下,实现从一个线程成功地访问到另一个线程创建的控件,那就要使用到C#的方法回调机制了。
方法二、使用回调函数
1、什么是回调函数?你创建一个函数,然后调用它就叫做调用,但是你在调用一个函数的时候,还需要把一个函数提供给该函数,让这个函数来调用你的函数,那么你提供的函数就被称为回调函数(callback)
C#的方法回调函数(机制),也是建立在委托上面的。
使用回调函数,首先就得定义和声明一个回调函数:
//定义回调(设置一个委托)
private delegate void SetTextValueCallBack(int value);
//声明回调
private SetTextValueCallBack setCallBack;
然后在“测试”点击事件上进行实例化回调
//实例化回调
setCallBack = SetValue;
再创建实例化回调的方法SetValue
private void SetValue(int value)
{
textBox1.Text = value.ToString();
}
最后在Test方法当中利用控件中Invoke属性来进行整合。
private void Test()
{
for (int i = 1; i <= 10000; i++)
{
//textBox1.Text = i.ToString();
//这里的i参数是给回调函数的参数,即setCallBack,
textBox1.Invoke(setCallBack, i);
}
//Invoke()拥有控件的基础窗口句柄的线程上执行指定的委托,即在主线程上执行的委托
完整的代码为:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
//定义回调(设置一个委托)
private delegate void SetTextValueCallBack(int value);
//声明回调
private SetTextValueCallBack setCallBack;
//创建一个"测试",将1-10000给文本框赋值
private void button1_Click(object sender, EventArgs e)
{
//实例化回调
setCallBack = SetValue;
//创建线程调用方法
Thread th = new Thread(Test);
th.IsBackground = true;//设置为后台线程
th.Start();
}
//定义一个方法,使其将0—10000之间的数循环打印在文本框里面
private void Test()
{
for (int i = 1; i <= 10000; i++)
{
//textBox1.Text = i.ToString();
//Invoke()拥有控件的基础窗口句柄的线程上执行指定的委托,即在主线程上执行的委托
textBox1.Invoke(setCallBack, i);//这里的i参数是给回调函数的参数,即setCallBack,
}
}
private void SetValue(int value)
{
textBox1.Text = value.ToString();
}
private void Form1_Load(object sender, EventArgs e)
{
//屏蔽窗体加载事件当中C#编译器对跨线程访问的检查
//Control.CheckForIllegalCrossThreadCalls = false;
}
}
运行结果:
通过利用回调函数的方法,同样可以达到一样的效果,同时也能够满足C#的相关安全规范要求。
以下为利用回调函数的简写方法:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
Thread th = new Thread(Test);
th.IsBackground = true;
th.Start();
}
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);
}
}
}
}
在这个方法当中,是把回调函数的定义、声明和实例化都利用C#中Lambda的方法一次性的写在了Invoke属性里面。其中textBox1.InvokeRequired这个属性是用来判断是否有其他线程来访问它,有则为true,反之则是false,是专门用来解决C#当中跨线程的问题。
异步委托(简单方式)
之前我们就学习过同步线程,所谓同步那就是需要一步一步来,完成前面的之后后面的才可以继续进行,就比如说下载东西,如果是同步进行的话,我们在下载东西的同时就无法去做其他的事情,就只能够等到这一件事完成了之后才能继续进行后面的任务,这就会造成不便。所以这些时候就得使用到异步方法了,所谓异步就相当于是在同一时刻,能够同时去做不一样的事情。在C#当中,同步线程当中可以用到Invoke方法,异步线程那就能用到BeginInvoke这个方法了。
先看一段代码:
static void Main(string[] args)
{
Console.WriteLine("主线程ID" + Thread.CurrentThread.ManagedThreadId);
//定义一个委托并初始化
Func<int, int, string> delFunc = (a, b) =>
{
//由于使用了BeginInvoke方法,所以就开启一个新的线程去执行,所以就称为异步线程
Console.WriteLine("异步线程ID:" + Thread.CurrentThread.ManagedThreadId);
return (a + b).ToString();
};
delFunc.BeginInvoke(1, 2, null, null);
Console.ReadLine();
}
}
在以上的代码中,首先是定义了一个委托,然后在定义的委托delFunc中用到了BeginInvoke方法,运行结果如下:
由上面的结果我们可以看到,主线程的ID为10,异步线程的ID为6,两个线程的ID不一样,说明是在两个不同的线程在执行,也就是说在委托当中使用了BeginInvoke方法后,就开启了一个新的线程去执行这个委托里面的功能,因此就称为异步委托,它的本质就是使用了线程池的线程去执行委托指向的方法。不是用主线程去执行。
BeginInvoke方法的详解:
(1)BeginInvoke方法用于异步委托的执行开始。有返回值IAsyncResult,但并不是执行委托方法的返回值
(2)BeginInvoke()是可以接受多个参数的,它的参数个数和参数类型取决于定义委托时的参数个数和类型,无论它有多少个参数,最后两个参数都是不变的。
(3)倒数第二个参数为回调函数,暂时为null。
(4) 倒数第一个参数时给回调函数的参数,参数为null。
BeginInvoke方法是有返回值IAsyncResult的,那么如何来获取这个返回值呢?这也是异步委托的优势,因为手写线程只能执行没有返回值的委托,因此需要执行有返回值的委托,就需要用到异步委托。
(1)首先第一步得拿到BeginInvoke方法的返回值result
IAsyncResult result = delFunc.BeginInvoke(1, 2, null, null);
(2)使用委托中EndInvoke()方法存放result,然后等待异步委托完成了之后进行输出;
string str = delFunc.EndInvoke(result);
Console.WriteLine(str);
完整代码:
static void Main(string[] args)
{
Console.WriteLine("主线程ID" + Thread.CurrentThread.ManagedThreadId);
//定义一个委托并初始化
Func<int, int, string> delFunc = (a, b) =>
{
//由于使用了BeginInvoke方法,所以就开启一个新的线程去执行,所以就称为异步线程
Console.WriteLine("异步线程ID:" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(3000);
return (a + b).ToString();
};
IAsyncResult result = delFunc.BeginInvoke(1, 2, null, null);
//判断异步委托是否执行完成,如果完成返回true
//while(!result.IsCompleted)
while(!result.AsyncWaitHandle.WaitOne(1000))
/*使用WaitOne(),自定义一个等待时间,
* 如果在这个等待时间内异步线程还没有完成,
* 就执行while里面的主线程逻辑,反之就不会执行
* 也可以用这样的方法在异步委托还没有完成之前
* 去做其他的事情。
*/
{
Thread.Sleep(100);
Console.WriteLine("主线程还在进行当中,请稍后~~~~");
}
//添加EndInvoke()方法,会阻塞线程,直到异步委托完成了之后才能往下执行。
string str = delFunc.EndInvoke(result);
Console.WriteLine(str);
Console.ReadLine();
}
}
回调函数是干什么用的?什么时候会被执行?
回调函数:是异步委托执行完成之后,再来调回调函数,也就是说异步委托执行完成之后,还需要处理的事情就可以用回调函数来完成。
在上面异步委托(简单方式)的基础上,首先需要定义一个回调函数:
public static void MyAsyncCallBack(IAsyncResult ar)
{
//需要通过回调函数完成的任务;
}
在异步委托上把回调函数的参数修正,并输入传给回调函数的参数;
IAsyncResult result = delFunc.BeginInvoke(1, 2, MyAsyncCallBack, "123");
在回调函数中拿到异步委托的执行结果
前面有讲过就是可以通过EndInvoke方法,既可以拿到委托执行的结果,所以关键就是如果拿到异步委托。
方法一:
//1、把ar强行转为实例类型
AsyncResult result = (AsyncResult)ar;
//2、通过实例类型中AsyncDelegate属性拿到异步委托,然后再强转为它的实例类型
var del = (Func<int, int, string>)result.AsyncDelegate;
//3、通过EndInvoke方法,参数ar的实例类型或者就为ar,就能得到异步委托执行结果
string returnValue = del.EndInvoke(ar);
Console.WriteLine("执行回调函数的线程ID为:" + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("执行回调函数的返回值是:" + returnValue);
//4、拿到异步委托当中传给回调函数的参数
Console.WriteLine("传给回调函数的参数是" + ar.AsyncState);
方法二:
var del = (Func<int, int, string>)ar.AsyncState;
string returnValue = del.EndInvoke(ar);
Console.WriteLine("异步委托的返回值为:" + returnValue);
Console.ReadLine();
如果你使用到方法二的话你需要在异步委托的定义中将传给的回调函数的参数给修改,不能为实际值。
IAsyncResult result = delFunc.BeginInvoke(1, 2, MyAsyncCallBack,delFunc);
完整代码:
static void Main(string[] args)
{
Console.WriteLine("主线程ID" + Thread.CurrentThread.ManagedThreadId);
//定义一个委托并初始化
Func<int, int, string> delFunc = (a, b) =>
{
//由于使用了BeginInvoke方法,所以就开启一个新的线程去执行,所以就称为异步线程
Console.WriteLine("异步线程ID:" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
return (a + b).ToString();
};
IAsyncResult result = delFunc.BeginInvoke(1, 2, MyAsyncCallBack, "123");
Console.ReadLine();
}
public static void MyAsyncCallBack(IAsyncResult ar)
{
#region 取得异步委托执行结果的返回值方法一
//把ar强行转为实例类型
AsyncResult result = (AsyncResult)ar;
//通过实例类型中AsyncDelegate属性拿到异步委托,然后再强转为它的实例类型
var del = (Func<int, int, string>)result.AsyncDelegate;
//通过EndInvoke方法,参数ar的实例类型或者就为ar,就能得到异步委托执行结果
string returnValue = del.EndInvoke(ar);
Console.WriteLine("执行回调函数的线程ID为:" + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("执行回调函数的返回值是:" + returnValue);
//拿到异步委托当中传给回调函数的参数
Console.WriteLine("传给回调函数的参数是" + ar.AsyncState);
#endregion
#region 方法二
//var del = (Func)ar.AsyncState;
//string returnValue = del.EndInvoke(ar);
//Console.WriteLine("异步委托的返回值为:" + returnValue);
//Console.ReadLine();
#endregion
}
}