跨线程访问
点击“测试”,创建一个线程,从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)、定义、声明回调。
//定义回调 private delegate void DoSomeCallBack(Type para); //声明回调 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(); } } }
同步和异步
同步和异步是对方法执行顺序的描述。
同步:等待上一行完成计算之后,才会进入下一行。
例如:请同事吃饭,同事说很忙,然后就等着同事忙完,然后一起去吃饭。
异步:不会等待方法的完成,会直接进入下一行,是非阻塞的。
例如:请同事吃饭,同事说很忙,那同事先忙,自己去吃饭,同事忙完了他自己去吃饭。
下面通过一个例子讲解同步和异步的区别:
1、新建一个winform程序,上面有两个按钮,一个同步方法、一个异步方法,在属性里面把输出类型改成控制台应用程序,这样可以看到输出结果,代码如下:

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace MyAsyncThreadDemo { public partial class Form1 : Form { public Form1() { InitializeComponent(); } /// <summary> /// 异步方法 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnAsync_Click(object sender, EventArgs e) { Console.WriteLine($"***************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId}"); Action<string> action = this.DoSomethingLong; // 调用委托(同步调用) action.Invoke("btnAsync_Click_1"); // 异步调用委托 action.BeginInvoke("btnAsync_Click_2",null,null); Console.WriteLine($"***************btnAsync_Click End {Thread.CurrentThread.ManagedThreadId}"); } /// <summary> /// 同步方法 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnSync_Click(object sender, EventArgs e) { Console.WriteLine($"****************btnSync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); int j = 3; int k = 5; int m = j + k; for (int i = 0; i < 5; i++) { string name = string.Format($"btnSync_Click_{i}"); this.DoSomethingLong(name); } } private void DoSomethingLong(string name) { Console.WriteLine($"****************DoSomethingLong {name} Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); long lResult = 0; for (int i = 0; i < 1000000000; i++) { lResult += i; } Console.WriteLine($"****************DoSomethingLong {name} End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************"); } } }
2、启动程序,点击同步,结果如下:
从上面的截图中能够很清晰的看出:同步方法是等待上一行代码执行完毕之后才会执行下一行代码。
点击异步,结果如下:
从上面的截图中看出:当执行到action.BeginInvoke("btnAsync_Click_2",null,null);这句代码的时候,程序并没有等待这段代码执行完就执行了下面的End,没有阻塞程序的执行。
在刚才的测试中,如果点击同步,这时winform界面不能拖到,界面卡住了,是因为主线程(即UI线程)在忙于计算。
点击异步的时候,界面不会卡住,这是因为主线程已经结束,计算任务交给子线程去做。
在仔细检查上面两个截图,可以看出异步的执行速度比同步执行速度要快。同步方法执行完将近16秒,异步方法执行完将近6秒。
在看下面的一个例子,修改异步的方法,也和同步方法一样执行循环,修改后的代码如下:
private void btnAsync_Click(object sender, EventArgs e) { Console.WriteLine($"***************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId}"); //Action<string> action = this.DoSomethingLong; //// 调用委托(同步调用) //action.Invoke("btnAsync_Click_1"); //// 异步调用委托 //action.BeginInvoke("btnAsync_Click_2",null,null); Action<string> action = this.DoSomethingLong; for (int i = 0; i < 5; i++) { //Thread.Sleep(5); string name = string.Format($"btnAsync_Click_{i}"); action.BeginInvoke(name, null, null); } Console.WriteLine($"***************btnAsync_Click End {Thread.CurrentThread.ManagedThreadId}"); }
结果如下:
从截图中能够看出:同步方法执行是有序的,异步方法执行是无序的。异步方法无序包括启动无序和结束无序。启动无序是因为同一时刻向操作系统申请线程,操作系统收到申请以后,返回执行的顺序是无序的,所以启动是无序的。结束无序是因为虽然线程执行的是同样的操作,但是每个线程的耗时是不同的,所以结束的时候不一定是先启动的线程就先结束。从上面同步方法中可以清晰的看出:btnSync_Click_0执行时间耗时不到3秒,而btnSync_Click_1执行时间耗时超过了3秒。可以想象体育比赛中的跑步,每位运动员听到发令枪起跑的顺序不同,每位运动员花费的时间不同,最终到达终点的顺序也不同。
总结一下同步方法和异步方法的区别:
1、同步方法由于主线程忙于计算,所以会卡住界面。
异步方法由于主线程执行完了,其他计算任务交给子线程去执行,所以不会卡住界面,用户体验性好。
2、同步方法由于只有一个线程在计算,所以执行速度慢。
异步方法由多个线程并发运算,所以执行速度快,但并不是线性增长的(资源可能不够)。多线程也不是越多越好,只有多个独立的任务同时运行,才能加快速度。
3、同步方法是有序的。
异步多线程是无序的:启动无序,执行时间不确定,所以结束也是无序的。一定不要通过等待几毫秒的形式来控制线程启动/执行时间/结束。
回调
先来看看异步多线程无序的例子:
在界面上新增一个按钮,实现代码如下:
private void btnAsyncAdvanced_Click(object sender, EventArgs e) { Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); Action<string> action = this.DoSomethingLong; action.BeginInvoke("btnAsyncAdvanced_Click", null, null); // 需求:异步多线程执行完之后再打印出下面这句 Console.WriteLine($"到这里计算已经完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。"); Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); }
运行结果:
从上面的截图中看出,最终的效果并不是我们想要的效果,而且打印输出的还是主线程。
既然异步多线程是无序的,那我们有没有什么办法可以解决无序的问题呢?办法当然是有的,那就是使用回调,.NET框架已经帮我们实现了回调:
BeginInvoke的第二个参数就是一个回调,那么AsyncCallback究竟是什么呢?F12查看AsyncCallback的定义:
发现AsyncCallback就是一个委托,参数类型是IAsyncResult,明白了AsyncCallback是什么以后,将上面的代码进行如下的改造:
private void btnAsyncAdvanced_Click(object sender, EventArgs e) { Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); Action<string> action = this.DoSomethingLong; // 定义一个回调 AsyncCallback callback = p => { Console.WriteLine($"到这里计算已经完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。"); }; // 回调作为参数 action.BeginInvoke("btnAsyncAdvanced_Click", callback, null); Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); }
运行结果:
上面的截图中可以看出,这就是我们想要的效果,而且打印是子线程输出的,但是程序究竟是怎么实现的呢?我们可以进行如下的猜想:
程序执行到BeginInvoke的时候,会申请一个基于线程池的线程,这个线程会完成委托的执行(在这里就是执行DoSomethingLong()方法),在委托执行完以后,这个线程又会去执行callback回调的委托,执行callback委托需要一个IAsyncResult类型的参数,这个IAsyncResult类型的参数是如何来的呢?鼠标右键放到BeginInvoke上面,查看返回值:
发现BeginInvoke的返回值就是IAsyncResult类型的。那么这个返回值是不是就是callback委托的参数呢?将代码进行如下的修改:
private void btnAsyncAdvanced_Click(object sender, EventArgs e) { // 需求:异步多线程执行完之后再打印出下面这句 Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); Action<string> action = this.DoSomethingLong; // 无序的 //action.BeginInvoke("btnAsyncAdvanced_Click", null, null); IAsyncResult asyncResult = null; // 定义一个回调 AsyncCallback callback = p => { // 比较两个变量是否是同一个 Console.WriteLine(object.ReferenceEquals(p,asyncResult)); Console.WriteLine($"到这里计算已经完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。"); }; // 回调作为参数 asyncResult= action.BeginInvoke("btnAsyncAdvanced_Click", callback, null); Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); }
运行结果:
这里可以看出BeginInvoke的返回值就是callback委托的参数。
现在我们可以使用回调解决异步多线程无序的问题了。
获取委托异步调用的返回值
使用EndInvoke可以获取委托异步调用的返回值,请看下面的例子:
private void btnAsyncReturnVlaue_Click(object sender, EventArgs e) { // 定义一个无参数、int类型返回值的委托 Func<int> func = () => { Thread.Sleep(2000); return DateTime.Now.Day; }; // 输出委托同步调用的返回值 Console.WriteLine($"func.Invoke()={func.Invoke()}"); // 委托的异步调用 IAsyncResult asyncResult = func.BeginInvoke(p => { Console.WriteLine(p.AsyncState); },"异步调用返回值"); // 输出委托异步调用的返回值 Console.WriteLine($"func.EndInvoke(asyncResult)={func.EndInvoke(asyncResult)}"); }
运行结果:
原文链接:https://www.cnblogs.com/dotnet261010/p/6159984.html
来源:https://www.cnblogs.com/zhaoyl9/p/12191989.html