异步多线程 1 利用委托Action/Func
基础概念
进程:程序运行时,占据的计算资源的合集,称为进程,如qq、word都是一个进程。
进程之间不会互相干扰。
线程:依托于进程,一个进程可以有多个线程,它是程序执行的最小单位,响应操作的最小执行流,如果说qq是一个进程,那么聊天,发图片,截图,加好友这些,都可以算作线程。
多线程:指在一个进程下有多个线程并发执行。
多线程应用在生活中随处可见,例如迅雷,有没有发现迅雷是可以同时下载东西的?例如同时下载A,B,A下载进度到53.4%,B下载进度到47.1%,有时A速度快些,有时B速度快些,反正能确定的是A,B都在下载内容,而不是一定要等A下载完后,B才可以开始下载,这也是多线程的作用。因此,多线程强调”同时,一起进行“,而不是单一的顺下操作。
多线程Thread类:一个封装的类,是.net对线程对象的封装,通过Thread类去完成的操作,最终是通过向操作系统请求得到的执行流。
直接代码演示一下利用委托Aciton开启多线程
Action.BeginInvoke 异步委托
static void Main(string[] args) { AsyncMethod1(); Console.ReadLine(); } private static void AsyncMethod1() { Console.WriteLine("AsyncMethod1开始线程:"+Thread.CurrentThread.ManagedThreadId.ToString()); Action<string> action = DoSomething; action.Invoke("jack");//同步 action("rose");//同步 action.BeginInvoke("ok", null, null);//异步 Console.WriteLine("AsyncMethod1结束线程:" + Thread.CurrentThread.ManagedThreadId.ToString()); } private static void DoSomething(string name) { Console.WriteLine("开始线程id:" + Thread.CurrentThread.ManagedThreadId.ToString() + " name:" + name); long result = 0; for (int i = 0; i < 100000000; i++) { result += i; } Console.WriteLine("结束线程id:" + Thread.CurrentThread.ManagedThreadId.ToString() + " name:" + name); }
运行结果是这样
可以看到,同步方法,用的是同一个线程 1,而异步方法,用的是新的线程 3,当输出了“AsyncMethod1结束”后,异步方法才调用完成,其实就是用资源换性能。
如果你在异步代码运行时看一下cpu,会发现占用情况比同步方法执行时高多了,但是也不是线性增长的,因为资源总有上限,资源调度也需要成本。
而且异步方法有个很大的问题就是,不同线程的开始和结束是无序的,如果一套顺序的业务流程代码,用异步去执行,很可能乱掉,当然这也有解决方法,只是从异步多线程的原理来看,会无法预测执行顺序。
当我们需要在异步多线程方法中加入日志,通常不会去把写日志的方法加在业务操作的方法中,而是利用回调函数,以上例为例。
private static void AsyncMethod2() { Console.WriteLine("AsyncMethod2开始线程:" + Thread.CurrentThread.ManagedThreadId.ToString()); Action<string> action = DoSomething; AsyncCallback asyncCallback = (ar) => { Console.WriteLine(ar.AsyncState.ToString()+":"+ Thread.CurrentThread.ManagedThreadId.ToString()); if (ar.AsyncState.ToString() == "ok") AddLog("张三"); }; action.BeginInvoke("张三", asyncCallback, "ok");//异步 Console.WriteLine("AsyncMethod2结束线程:" + Thread.CurrentThread.ManagedThreadId.ToString()); } private static void AddLog(string name) { Console.WriteLine(name + "写日志:" + Thread.CurrentThread.ManagedThreadId.ToString()); }
Action的BeginInvoke的第二个和第三个参数分别是,回调函数,传入的参数。
运行结果是这样
可以看到,主线程1执行完成后就释放了,线程3自己去执行Dosomething,并且在执行完后执行AddLog,这样既不会阻塞主线程,又可以监控到异步方法并写日志。
IAsyncResult 等待
可以利用BeginInvoke的返回值进行异步操作的进度判断,例如做上传文件的进度提示,异步业务等待提示等等。
改一下AsyncMethod2
private static void AsyncMethod2() { Console.WriteLine("AsyncMethod2开始线程:" + Thread.CurrentThread.ManagedThreadId.ToString()); Action<string> action = DoSomething; AsyncCallback asyncCallback = (ar) => { Console.WriteLine(ar.AsyncState.ToString()+":"+ Thread.CurrentThread.ManagedThreadId.ToString()); if (ar.AsyncState.ToString() == "ok") AddLog("张三"); }; IAsyncResult asyncResult = action.BeginInvoke("张三", asyncCallback, "ok");//异步 while (!asyncResult.IsCompleted) { Console.WriteLine("AsyncMethod2还在异步调用DoSomething哦!线程:" + Thread.CurrentThread.ManagedThreadId.ToString()); Thread.Sleep(100);//休息100ms,不然死循环了咋整 } if (asyncResult.IsCompleted) { Console.WriteLine("AsyncMethod2异步调用DoSomething完成!线程:" + Thread.CurrentThread.ManagedThreadId.ToString()); } Console.WriteLine("AsyncMethod2结束线程:" + Thread.CurrentThread.ManagedThreadId.ToString()); }
当DoSomething方法还没有执行结束时,循环输出“AsyncMenthod2还在异步调用....”,并且休眠100ms,执行完成后输出“AsyncMenthod2异步调用....完成”。
结果是这样
这里的问题是, 等待完成的那个while循环的内部其实是由主线程1控制的,明显看到了1和3两个线程并行,直到3执行完1最后才执行完成,中间1其实一直被占用,并且不够精确,我在循环中设置了100ms的延迟,也就是说无论如何也有个100ms的延迟。
信号量WaitOne
private static void AsyncMethod2() { Console.WriteLine("AsyncMethod2开始线程:" + Thread.CurrentThread.ManagedThreadId.ToString()); Action<string> action = DoSomething; AsyncCallback asyncCallback = (ar) => { Console.WriteLine(ar.AsyncState.ToString()+":"+ Thread.CurrentThread.ManagedThreadId.ToString()); if (ar.AsyncState.ToString() == "ok") AddLog("张三"); }; IAsyncResult asyncResult = action.BeginInvoke("张三", asyncCallback, "ok");//异步 #region 信号量,精确 //发邮件 //发短信 //减库存 //通知业务人员 asyncResult.AsyncWaitHandle.WaitOne(); #endregion Console.WriteLine("AsyncMethod2结束线程:" + Thread.CurrentThread.ManagedThreadId.ToString()); }
这个例子的意思是,当我异步调用DoSomething的同时,我需要同时再去调用 发邮件,发短信等等别的方法,但是呢,我希望后续的方法要等我这个异步调用完成后再执行,于是用asyncResult.AsyncWaitHandle.WaitOne()来阻塞当前进程,接受到asyncResult发出的信号量才继续。
结果是这样,可以看到线程1是等待DoSomething完成后才完成的,而且跟回调方法无关,回调还是自己跑自己的,可以理解成,异步方法执行完,线程1剩下的代码和回调方法并发执行。
但是这样也有个问题,如果异步方法出了问题,岂不是像同步方法一样,卡死在这里?可以给WaitOne加上参数,让线程1最多阻塞1000ms,1000ms内执行完成,继续走,没执行完成,也不管还是继续走。
asyncResult.AsyncWaitHandle.WaitOne(1000);
结果,因为我这个Dosomething方法执行时间超过了1000ms,所以可以看到线程1等待了1000ms后发现线程3的异步方法还没执行完,不等了,直接走它的,然后异步方法执行完成后,再执行回调函数。
Func 获得返回值
了解Aciton的朋友会知道,Action委托没有返回值,如果需要返回值,可以用Func
private static void AsyncMethod2() { Console.WriteLine("AsyncMethod2开始线程:" + Thread.CurrentThread.ManagedThreadId.ToString()); #region Func 有返回值 Func<string, string> func = GetApi; IAsyncResult asyncResult = func.BeginInvoke("张三", null, null); string result = func.EndInvoke(asyncResult); Console.WriteLine("获取到返回值(" + result + ")线程:" + Thread.CurrentThread.ManagedThreadId.ToString()); #endregion Console.WriteLine("AsyncMethod2结束线程:" + Thread.CurrentThread.ManagedThreadId.ToString()); } private static string GetApi(string name) { Console.WriteLine("GetApi线程:" + Thread.CurrentThread.ManagedThreadId.ToString()); long result = 0; for (long i = 0; i < 1000000000; i++) { result += i; } return "我是Api返回的值:" + name; }
结果
另外这个返回值也可以放到回调中去使用,例如这样,我改一下func
Func<string, string> func = GetApi; IAsyncResult asyncResult = func.BeginInvoke("张三", ar => { AddLog(ar.AsyncState.ToString()); func.EndInvoke(ar); }, "ok");
结果
看,返回值在回调中获取,是线程3,而上例在回调外获取,是线程1。
需要注意的是,同一个委托的begininvoke,只能有一个endinvoke,你不能调用多次,否则报错。