1. 计算机概念
进程:应用程序在服务器上运行时占用的资源合集。进程之间互不干扰
线程:程序执行的最小单位,响应操作的最小执行单位。线程也有属于自己的计算资源,一个进程有多个线程。
多线程:一个进程中有多个线程在并发执行。
2.c#定义
多线程Thread类:是.netFramework对线程对象的封装,通过Thread来完成操作,其本质是通过向操作系统得到的执行流。因为线程这个东西是属于操作系统的,不是c#的,c#语言中只是封装了这个类来对它进行操作。
CurrentThread:当前线程;
类中的定义:
// // 摘要: // Gets the currently running thread. // // 返回结果: // A System.Threading.Thread that is the representation of the currently running // thread. public static Thread CurrentThread { get; }
ManagedThreadId:它是.Net平台给Thread起的名字,就是个int值,用来区分不同线程的,尽量不重复。
// // 摘要: // Gets a unique identifier for the current managed thread.(获取当前托管线程的唯一标识符。) // // 返回结果: // An integer that represents a unique identifier for this managed thread. public int ManagedThreadId { get; }
下面简单只有一个子线程的例子:
public partial class Form1 : Form { public Form1() { InitializeComponent(); } /// <summary> /// 同步方法 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_Click(object sender, EventArgs e) { Console.WriteLine($"******* **************btn_Click 同步方法 start {Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToLongDateString()}*************************"); int j = 0; int k = 1; int m = j + k; for (int i = 0; i < 5; i++) { DoSomethingLong($"btn_Click_{i}"); } Console.WriteLine($"**************************btn_Click 同步方法 end {Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToLongDateString()}******* ********"); } private void DoSomethingLong(string name) { Console.WriteLine($"**************************DoSomethingLong start {name}-{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToLongDateString()}***** *********"); long IResult = 0; for (int i = 0; i < 100000000; i++) { IResult = i; } Console.WriteLine($"**************************DoSomethingLong end {name}-{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToLongDateString()}***** *********"); } /// <summary> /// 异步方法 /// 任何的异步多线程都离不开委托-lambda /// </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; #region 同步执行委托 action.Invoke("btnAsync_Click"); #endregion #region 异步执行委托 此时会启用新线程调用DoSomethingLong方法,主线程继续向下执行,不必等待DoSomethingLong执行完成。 action.BeginInvoke("btnAsync_Click", null, null); #endregion Console.WriteLine($"**************************btnAsync_Click 异步方法 end {Thread.CurrentThread.ManagedThreadId}*************************"); } }
异步线程三大特点之不卡界面
做一个winform程序,界面如下:
代码如下,同步方法使用委托,异步方法启用多线程形式:
public partial class Form1 : Form { public Form1() { InitializeComponent(); } /// <summary> /// 委托的同步方法 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_Click(object sender, EventArgs e) { Console.WriteLine($"**************************btn_Click 同步方法 start {Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToLongDateString()}*************************"); //int j = 0; //int k = 1; //int m = j + k; Action<string> action = this.DoSomethingLong; for (int i = 0; i < 5; i++) { action.Invoke($"btnAsync_Click_{i}" ); } Console.WriteLine($"**************************btn_Click 同步方法 end {Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToLongDateString()}*** ****************"); } /// <summary> /// 异步方法 /// 任何的异步多线程都离不开委托-lambda /// </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; //#region 同步执行委托 //action.Invoke("btnAsync_Click"); //#endregion //#region 异步执行委托 此时会启用新线程调用DoSomethingLong方法,主线程继续向下执行,不必等待DoSomethingLong执行完成。 //action.BeginInvoke("btnAsync_Click", null, null); //#endregion for (int i = 0; i < 5; i++) { action.BeginInvoke($"btnAsync_Click_{i}", null, null); } Console.WriteLine($"**************************btnAsync_Click 异步方法 end {Thread.CurrentThread.ManagedThreadId}*************************"); } private void DoSomethingLong(string name) { Console.WriteLine($"**************************DoSomethingLong start {name}-{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToLongDateString()}**** ********"); long IResult = 0; for (int i = 0; i < 1000000000; i++) { IResult = i; } Console.WriteLine($"**************************DoSomethingLong end {name}-{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToLongDateString()}*** *********"); } }
最直观的感受是点击同步方法的时候界面是拖不动的,异步方法的时候DoSomethingLong没有执行完成照样可以随便拖动界面。
为什么同步单线程方法会卡?首先明确一点是线程是属于操作系统的,卡是因为没有线程来响应,因为此时主线程忙于计算,所以没法响应拖动操作。
异步多线程界面不卡是因为主线程不做计算,启用子线程来计算,所以此时主线程可以响应拖动操作。
在winform中启用线程的情况比较多,但是B/S开发应用场景也比较多,比如做日志等。
异步线程三大特点之资源换性能
在运行的时候其实可以看出来同步方法要比多线程运行的时候慢一点。
如下图:这是我没有执行方法的时候的CPU利用率,大约在20%左右。
点击同步方法之后 利用率冲到了50左右
而点击异步方法之后CPU利用率一度到达了100%
然后我们看一下具体的执行时间长度:
我们可以看到同步方法用了10秒左右,异步方法只是用了3秒左右,所以可以看到多线程是用资源换取了性能,但是需要注意的是不是线程越多性能越优,因为电脑本身的资源是有限的,我们现在用的BeginInvoke这个方法也是基于线程池的,而线程池也是有上限的。当然如果你是用下面这种形式,的确可以想弄多少线程就有多少,至于能不能运行,计算机能不能带起来那就另说了。
Thread t1 = new Thread(() => DoSomethingLong("")); t1.Start();
异步线程三大特点之无序性
如下2图:这是分2次点击异步方法产生的结果:
我们可以看出:多线程启动和结束都是无序性的,而且同一个线程同一个任务多次调用耗时也是不同的。之所以出现每次的耗时不同是因为操作系统的调度策略有关。因为系统会将CPU进行分片,同一时间段之中可能会来回切换做不同的事情,每次给你分配的时间也可能不同。之所以无序性是因为多线程几乎是同一时间向操作系统请求线程,又因为操作系统的调度策略导致CPU可能就随机抽取其中一个请求来分配线程,不是根据请求的先后顺序进行分配的。 可以通过设置优先级来确保线程的有序性。
补充:CPU分片
异步执行回调方法
案例1:每一个子线程执行完成之后加一个日志:
private void btn_Asyncadvanced_Click(object sender, EventArgs e) { Console.WriteLine($"**************************btn_Asyncadvanced_Click 异步方法 start {Thread.CurrentThread.ManagedThreadId}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}* ** "); Action<string> action = this.UpdateDB; AsyncCallback asyncCallback = arr => { //AsyncState是传进来的参数,是object类型,任何形式都能接收 Console.WriteLine($"日志:{ arr.AsyncState}"); }; action.BeginInvoke($"btnAsync_Click", asyncCallback, "测试"); Console.WriteLine($"**************************btn_Asyncadvanced_Click 异步方法 end {Thread.CurrentThread.ManagedThreadId} -{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} ** ****"); }
结果:
**************************btn_Asyncadvanced_Click 异步方法 end 1 -2021-09-16 18:49:19 ************************* **************************UpdateDB start btnAsync_Click-3-2021-09-16 18:49:19************************* **************************UpdateDB end btnAsync_Click-3-2021-09-16 18:49:21************************* 日志:测试
案例2:用户必须确定操作完成才能返回。比如上传文件,需要等到文件真正上传成功之后才能将上传的数据展示在界面上。之所以不用同步方法,但是万一数据过多,界面一旦卡死又没有其他提示,用户观感十分差,所以尽量通过异步解决。
现在有2点需求:一是文件上传之后才能展示数据,二是有个进度提示。只有主线程才能操作界面
注意:下面只是简单做个例子,真实上传文件的时候一开始是可以读到文件的size的,然后根据上传好的size,做个比例来展示进度。
#region IsComplete完成异步等待 /// <summary> /// 模仿一个业务 /// </summary> /// <param name="name"></param> private void UpdateDB(string name) { Console.WriteLine($"***** **********UpdateDB start {name}-{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}******** ****"); long IResult = 0; for (int i = 0; i < 1000000000; i++) { IResult = i; } Console.WriteLine($"** ***********UpdateDB end {name}-{Thread.CurrentThread.ManagedThreadId.ToString()}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}*************************"); } /// <summary> /// 高级异步方法 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_Asyncadvanced_Click(object sender, EventArgs e) { Console.WriteLine($"****** ************btn_Asyncadvanced_Click 异步方法 start {Thread.CurrentThread.ManagedThreadId}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}** **** **"); Action<string> action = this.UpdateDB; IAsyncResult asyncResult = action.BeginInvoke("文件上传", null, null); int i = 0; //IsCompleted用来描述异步动作是否完成,如果没完成就会一直循环,异步动作完成之后,IsCompleted就会变成true while (!asyncResult.IsCompleted) { //注意:真实上传文件的时候一开始是可以读到文件的size的,然后根据上传好的size,做个比例来展示进度。 if (i < 9) { this.showConsoleAndView($"进度{++i * 10}%"); } else { this.showConsoleAndView($"进度99.99%"); } //延时200毫秒,目的是为了防止过度循环 Thread.Sleep(200); } //上面的while循环完成之后执行下面代码 Console.WriteLine("完成文件上传"); Console.WriteLine($"***** *********btn_Asyncadvanced_Click 异步方法 end {Thread.CurrentThread.ManagedThreadId} -{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} ** ********"); } /// <summary> /// 进度展示 /// </summary> /// <param name="text"></param> private void showConsoleAndView(string text) { Console.WriteLine(text); this.lblProcessing.Text = text; } #endregion
下面是界面显示:进度99.99%是label显示的。
但是上面的方法是存在问题的:一是 this.lblProcessing.Text = text;这行代码有问题,我们首先要知道只有主线程才能操控界面,但是在while循环的时候就是主线程执行的,一直在忙着,是没有时间来将数据展示到界面上的,除非执行完btn_Asyncadvanced_Click方法的最后一行代码才会有时间展示,这时候也只能展示出来最后设置的一个进度信息,我们可以通过让主线程闲置下来,其他操作由子线程完成。第二个问题是while循环中一直有一个200毫秒的误差。
对于上面的第二个问题,我们可以通过信号量来解决
信号量就是用来做超时控制的,比如调用某个接口,超过了设置的时间之后就自动放弃。这个其实在实际开发任务中是很实用的,比如一个方法既要调用一个接口,又要处理其他任务,并且需要等到这个接口执行完成之后看接口返回的一些信息整个方法才能完成就可以用信号量来控制:
Action<string> action = this.UpdateDB; #region 信号量 //(1)启动异步接口 var async = action.BeginInvoke("开始调用接口", null, null); //(2)继续做其他任务 Console.WriteLine("做其他事情"); //(3)等待接口调用完成 阻止当前线程,直到子线程完成才继续向下执行。 async.AsyncWaitHandle.WaitOne(); //阻止当前线程,直到子线程完成才继续向下执行。 async.AsyncWaitHandle.WaitOne(-1); //阻止当前线程,最多只等待1000毫秒 async.AsyncWaitHandle.WaitOne(1000); //(4)方法执行成功 Console.WriteLine("调用接口成功"); #endregion
异步完成操作并获取返回值
#region 异步完成操作并获取返回值 Func<int> func = this.remoteService; { //无参数的异步执行 var asyncResult = func.BeginInvoke(null, null); //主线程阻塞在这里,直到计算完成并得到返回值之后才继续向下执行 int result = func.EndInvoke(asyncResult); Console.WriteLine($"完成结果:{result}"); } { //还可以放到回调里面执行 var asyncResult = func.BeginInvoke(arr=> { int result = func.EndInvoke(arr); }, null); } #endregion
多线程各版本的执行方式
1.0:
private void btnMultple_Click(object sender, EventArgs e) { Console.WriteLine($"**** *****btnMultple_Click 异步方法 start {Thread.CurrentThread.ManagedThreadId}-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}*************************"); #region 1.0 { //public delegate void ThreadStart();ThreadStart就是一个无参数的委托 Thread thread = new Thread(new ThreadStart(() => { Console.WriteLine($"start-{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(2000); Console.WriteLine($"end-{Thread.CurrentThread.ManagedThreadId}"); })); thread.Start(); } #endregion //上面的while循环完成之后执行下面代码 Console.WriteLine("完成文件上传"); Console.WriteLine($"**** ******btnMultple_Click 异步方法 end {Thread.CurrentThread.ManagedThreadId} -{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} *************************"); }
结果:
**************************btnMultple_Click 异步方法 start 1-2021-09-17 11:27:26************************* 完成文件上传 **************************btnMultple_Click 异步方法 end 1 -2021-09-17 11:27:26 ************************* start-3 end-3
为什么要对这个版本的写法进行升级呢?
因为Thread的API很丰富,有很多方法,比如下面这些:
thread.Suspend(); thread.Resume(); thread.Join(); thread.IsBackground=true; thread.Abort();
比如Suspend方法,可以让当前线程挂起,每个方法都有不同的功能,但是有一个非常重要的一点就是线程是属于操作系统的,不是.net平台的,.net平台只是封装了Thread类来向操作系统获取线程资源,而CPU又有时间分片的概念,比如你请求了Suspend挂起方法,可能当前CPU根本没时间搭理你,响应不是非常精确灵敏的,是很难控制的。还有一点,上面也说过,通过上面方法开启线程是可以任意开启无数个线程的,没有上限控制,万一线程开多了很容易造成死机的。
2.0
这个版本推出了线程池ThreadPool。设计思想是设计一个容器,容器中提前申请一定数量的线程,需要使用线程之后直接从容器中取,使用完成之后再放回容器(就是通过控制状态来实现的)。如果需要的数量已经超过线程池的最大限制数了,那就需要排队等待
下面是其中一个方法。
// // 摘要: // 将方法排入队列以便执行,并指定包含该方法所用数据的对象。 此方法在有线程池线程变得可用时执行。 // // 参数: // callBack: // System.Threading.WaitCallback,它表示要执行的方法。 // // state: // 包含方法所用数据的对象。 // // 返回结果: // 如果此方法成功排队,则为 true;如果无法将该工作项排队,则引发 System.NotSupportedException。 // // 异常: // T:System.NotSupportedException: // 承载公共语言运行时 (CLR),并且主机不支持此操作。 // // T:System.ArgumentNullException: // callBack 为 null。 [SecuritySafeCritical] public static bool QueueUserWorkItem(WaitCallback callBack, object state);
其中的WaitCallback是一个有参数无返回值的委托:
// // 摘要: // 表示要由线程池线程执行的回调方法。 // // 参数: // state: // 包含回调方法要使用的信息的对象。 [ComVisible(true)] public delegate void WaitCallback(object state);
代码演示:
#region 2.0 { //将方法排入队列以便执行,并指定包含该方法所用数据的对象。 此方法在有线程池线程变得可用时执行。 ThreadPool.QueueUserWorkItem(new WaitCallback(arr => { Console.WriteLine($"start-{Thread.CurrentThread.ManagedThreadId}-{arr}"); Thread.Sleep(2000); Console.WriteLine($"end-{Thread.CurrentThread.ManagedThreadId}-{arr}"); }), "测试"); } #endregion
结果:
****** **btnMultple_Click 异步方法 start 1-2021-09-17 11:50:02************************* **** *****btnMultple_Click 异步方法 end 1 -2021-09-17 11:50:02 ************************* start-3-测试 end-3-测试
2.0版本的API又太少了,实际开发中又不方便。
3.0
出现了Task。Task被很多人称之为多线程的最佳实践,因为Task线程全部都是线程池线程,此外还提供了非常丰富的API.
#region 3.0 { Action action = () => { Console.WriteLine($"start-{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(2000); Console.WriteLine($"end-{Thread.CurrentThread.ManagedThreadId}"); }; Task task = new Task(action); task.Start(); } #endregion
Parallel:主要用于并发任务的执行,在启动多线程的同时,主线程也参与计算,相当于节省了一个线程,但是对应的在没有计算完成之前是没法对界面的操控做出响应的。
{ Parallel.Invoke(() => { Console.WriteLine($"start1-{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(2000); Console.WriteLine($"end1-{Thread.CurrentThread.ManagedThreadId}"); }, () => { Console.WriteLine($"start2-{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(2000); Console.WriteLine($"end2-{Thread.CurrentThread.ManagedThreadId}"); }); }
结果:
****** **btnMultple_Click 异步方法 start 1-2021-09-17 13:00:45************************* start1-1 start2-5 end1-1 end2-5 **** *****btnMultple_Click 异步方法 end 1 -2021-09-17 13:00:47 *************************
从这个结果和Task的结果做对比就可以看出主线程是参与计算的。