C# 委托高级应用----线程——创建无阻塞的异步调用(一)
前言
本文大部分内容来自于mikeperetz的Asynchronous Method Invocation及本人的一些个人体会所得,希望对你有所帮助。原英文文献可以在codeproject中搜索到。
介绍
这篇文章将介绍异步调用的实现机制及如何调用异步方法。大多数.NET开发者在经过delegate、Thread、AsynchronousInvocation之后,通常都会对以上概念产生混淆及误用。实际上,以上概念是.NET2.0版本中对并行编程的核心支持,基于概念上的错误认识有可能导致在实际的编程中,无法利用异步调用的特性优化我们的程序,例如大数据量加载引起的窗体”假死”。事实上这并不是一个困难的问题,该文将以一种逐层深入、抽丝剥茧的方式逐渐深入到异步编程的学习中。
同步与异步
大多数人并不喜欢阅读大量的文字说明,而喜欢直接阅读代码,因此,我们在下文中将主要以代码的形式阐述同步与异步的调用。
同步方法调用
假设我们有一个函数,它的功能是将当前线程挂起3秒钟。
- staticvoid Sleep()
- {
- Thread.Sleep();
- }
通常,当你的程序在调用Sleep后,它将等待3秒钟的时间,在这3秒钟时间内,你不能做任何其他操作。3秒之后,控制权被交回给调用线程(通常也就是你的主线程,即WinForm程序的UI线程)。这种类型的调用称为同步,本次调用顺序如下:
● 调用Sleep();
● Sleep()执行中;
● Sleep()执行完毕,控制权归还调用线程。
我们再次调用Sleep()函数,不同的是,我们要基于委托来完成这次调用。一般为了将函数绑定在委托中,我们要定义与函数返回类型、参数值完全一致的委托,这稍有点麻烦。但.NET内部已经为我们定义好了一些委托,例如MethodInvoker,这是一种无返回值、无参数的委托签名,这相当于你自定义了一种委托:
- public delegate void SimpleHandler();
执行以下代码:
- MethodInvoker invoker =new MethodInvoker(Sleep);
- invoker.Invoke();
我们使用了委托,但依然是同步的方式。主线程仍然要等待3秒的挂起,然后得到响应。
注意:Delegate.Invoke是同步方式的。
异步方法调用
如何在调用Sleep()方法的同时,使主线程可以不必等待Sleep()的完成,一直能够得到相应呢?这很重要,它意味着在函数执行的同时,主线程依然是非阻塞状态。在后台服务类型的程序中,非阻塞的状态意味着该应用服务可以在等待一项任务的同时去接受另一项任务;在传统的WinForm程序中,意味着主线程(即UI线程)依然可以对用户的操作得到响应,避免了”假死”。我们继续调用Sleep()函数,但这次要引入BeginInvoke。
- MethodInvoker invoker =new MethodInvoker(Sleep);
- invoker.BeginInvoke(null, null);
● 注意BeginInvoke这行代码,它会执行委托所调用的函数体。同时,调用BeginInvoke方法的线程(以下简称为调用线程)会立即得到响应,而不必等待Sleep()函数 的完成。
● 以上代码是异步的,调用线程完全可以在调用函数的同时处理其他工作,但是不足的是我们仍然不知道对于Sleep()函数的调用何时会结束,这是下文将要解决的问 题。
● BeginInvoke可以以异步的方式完全取代Invoke,我们也不必担心函数包含参数的情况,下文介绍传值问题。
注意:Delegate.BeginInvoke是异步方式的。如果你要执行一项任务,但并不关心它何时完成,我们就可以使用BeginInvoke,它不会带来调用线程的阻塞。
对于异步调用,.NET内部究竟做了什么?
一旦你使用.NET完成了一次异步调用,它都需要一个线程来处理异步工作内容(以下简称异步线程),异步线程不可能是当前的调用线程,因为那样仍然会造成调用线程的阻塞,与同步无异。事实上,.NET会将所有的异步请求队列加入线程池,以线程池内的线程处理所有的异步请求。对于线程池似乎不必了解的过于深入,但我们仍需要关注以下几点内容:
● Sleep()的异步调用会在一个单独的线程内执行,这个线程来自于.NET线程池。
● .NET线程池默认包含25个线程,你可以改变这个值的上限,每次异步调用都会使用其中某个线程执行,但我们并不能控制具体使用哪一个线程。
● 线程池具备最大线程数目上限,一旦所有的线程都处于忙碌状态,那么新的异步调用将会被置于等待队列,直到线程池产生了新的可用线程,因此对于大量异步请 求,我们有必要关注请求数量,否则可能造成性能上的影响。
简单了解线程池
为了暴露线程池的上限,我们修改Sleep()函数,将线程挂起的时间延长至30s。在代码的运行输出结果中,我们需要关注以下内容:
● 线程池内的可用线程数量。
● 异步线程是否来自于线程池。
● 线程托管ID值。
上文已经提到,.NET线程池默认包含25个线程,因此我们连续调用30次异步方法,这样可以在第25次调用后,看看线程池内部究竟发生了什么。
- privatevoid Sleep()
- {
- int intAvailableThreads, intAvailableIoAsynThreds;
- // 取得线程池内的可用线程数目,我们只关心第一个参数即可
- ThreadPool.GetAvailableThreads(out intAvailableThreads,
- out intAvailableIoAsynThreds);
- // 线程信息
- string strMessage =
- String.Format("是否是线程池线程:{0},线程托管ID:{1},可用线程数:{2}",
- Thread.CurrentThread.IsThreadPoolThread.ToString(),
- Thread.CurrentThread.GetHashCode(),
- intAvailableThreads);
- Console.WriteLine(strMessage);
- Thread.Sleep();
- }
- privatevoid CallAsyncSleep30Times()
- {
- // 创建包含Sleep函数的委托对象
- MethodInvoker invoker =new MethodInvoker(Sleep);
- for (int i =; i <; i++)
- {
- // 以异步的形式,调用Sleep函数30次
- invoker.BeginInvoke(null, null);
- }
- }
对于输出结果,我们可以总结为以下内容:
● 所有的异步线程都来自于.NET线程池。
● 每次执行一次异步调用,便产生一个新的线程;同时可用线程数目减少。
● 在执行异步调用25次后,线程池中不再有空闲线程。此时,应用程序会等待空闲线程的产生。
● 一旦线程池内产生了空闲线程,它会立即被分配给异步任务等待队列,之后线程池中仍然不具备空闲线程,应用程序主线程进入挂起状态继续等待空闲线程,这样 的调用一直持续到异步调用被执行完30次。
针对以上结果,我们对于异步调用可以总结为以下内容:
● 每次异步调用都在新的线程中执行,这个线程来自于.NET线程池。
● 线程池有自己的执行上限,如果你想要执行多次耗费时间较长的异步调用,那么线程池有可能进入一种”线程饥饿”状态,去等待可用线程的产生。
BeginInvoke和EndInvoke
我们已经知道,如何在不阻塞调用线程的情况下执行一个异步调用,但我们无法得知异步调用的执行结果,及它何时执行完毕。为了解决以上问题,我们可以使用EndInvoke。EndInvoke在异步方法执行完成前,都会造成线程的阻塞。因此,在调用BeginInvoke之后调用EndInvoke,效果几乎完全等同于以阻塞模式执行你的函数(EndInvoke会使调用线程挂起,一直到异步函数执行完毕)。但是,.NET是如何将BeginInvoke和EndInvoke进行绑定呢?答案就是IAsyncResult。每次我们使用BeginInvoke,返回值都是IAsyncResult类型,它是.NET追踪异步调用的关键值。每次异步调用之后的结果如何?如果要了解具体执行结果,IAsyncResult便可视为一个标签。通过这个标签,你可以了解异步调用何时执行完毕,更重要的是,它可以保存异步调用的参数传值,解决异步函数上下文问题。
我们现在通过几个例子来了解IAsyncResult。如果之前对它了解不多,那么就需要耐心的将它领悟,因为这种类型的调用是.NET异步调用的关键内容。
- class Program
- {
- static private void SleepOneSecond(int UserID)
- {
- // 当前线程挂起1秒
- Thread.Sleep();
- Console.WriteLine("你输入的UserID为:{0}",UserID );
- }
- public delegate void MethodInvoker(int UserID);
- public static void Main(string[] args)
- {
- // 创建一个指向SleepOneSecond的委托
- MethodInvoker invoker =new MethodInvoker(SleepOneSecond);
- Console.Write("请输入UserID");
- int UserID = Convert.ToInt32(Console.ReadLine());
- // 开始执行SleepOneSecond,但这次异步调用我们传递一些参数
- // 观察Delegate.BeginInvoke()的第二个参数
- for (int i = ; i < ; i++)
- {
- IAsyncResult tag = invoker.BeginInvoke(UserID , null, "passing some " + i + " state");
- // 应用程序在此处会造成阻塞,直到SleepOneSecond执行完成
- invoker.EndInvoke(tag);
- // EndInvoke执行完毕,取得之前传递的参数内容
- string strState = (string)tag.AsyncState;
- Console.WriteLine("EndInvoke的传递参数" + strState);
- }
- Console.ReadKey();
- }
- }
- 请输入UserID
- 你输入的UserID为:
- EndInvoke的传递参数passing some state
- 你输入的UserID为:
- EndInvoke的传递参数passing some state
- 你输入的UserID为:
- EndInvoke的传递参数passing some state
回到文章初始提到的”窗体动态更新”问题,如果你将上述代码运行在一个WinForm程序中,会发现窗体依然陷入”假死”。对于这种情况,你可能会陷入疑惑:之前说异步函数都执行在线程池中,因此可以肯定异步函数的执行不会引起UI线程的忙碌,但为什么窗体依然陷入了”假死”?问题就在于EndInvoke。EndInvoke此时扮演的角色就是”线程锁”,它充当了一个调用线程与异步线程之间的调度器,有时调用线程需要使用异步函数的执行结果,那么调度线程就需要在异步执行完之前一直等待,直到得到结果方可继续运行。EndInvoke一方面负责监听异步函数的执行状况,一方面将调用线程挂起。
因此在Win Form环境下,UI线程的”假死”并不是因为线程忙碌造成,而是被EndInvoke”善意的”暂时封锁,它只是为了等待异步函数的完成。
我们可以对EndInvoke总结如下:
● 在执行EndInvoke时,调用线程会进入挂起状态,一直到异步函数执行完成。
● 使用EndInvoke可以使应用程序得知异步函数何时执行完毕。
● 如果将上述写法称为”异步”,你一定觉得这种”异步”徒具其名,虽然知道异步函数何时执行完毕,也得到了异步函数的传值,但我们的调用线程仍然会等待函数执行完毕,在等待过程中线程阻塞,实际上与同步调用无异。
如何捕捉异常?
现在我们把问题稍微复杂化,考虑异步函数抛出异常的一种情形。我们需要了解在何处捕捉到异常,是BeginInvoke,还是EndInvoke?甚至是有没有可能无法捕捉异常?答案是EndInvoke。BeginInvoke的工作只是开始线程池对于异步函数的执行工作,EndInvoke则需要处理函数执行完成的所有信息,包括其中产生的异常。
- class Program
- {
- public delegate void MethodInvoker();
- static private void SleepOneSecond()
- {
- // 当前线程挂起1秒
- Thread.Sleep();
- throw new Exception("Here Is An Async Function Exception");
- }
- public static void Main(string[] args)
- {
- // 创建一个指向SleepOneSecond的委托
- MethodInvoker invoker =new MethodInvoker(SleepOneSecond);
- // 开始执行SleepOneSecond,但这次异步调用我们传递一些参数
- // 观察Delegate.BeginInvoke()的第二个参数
- IAsyncResult tag = invoker.BeginInvoke(null, "passing some state");
- try
- {
- // 应用程序在此处会造成阻塞,直到SleepOneSecond执行完成
- invoker.EndInvoke(tag);
- }
- catch (System.Exception ex)
- {
- Console.WriteLine(ex.Message);
- }
- // EndInvoke执行完毕,取得之前传递的参数内容
- string strState = (string)tag.AsyncState;
- Console.WriteLine("EndInvoke的传递参数" + strState);
- Console.ReadKey();
- }
- }
- Here Is An Async Function Exception
- EndInvoke的传递参数passing some state
执行以上代码后,你将发现只有在使用EndInvoke时,才会捕捉到异常,否则异常将丢失。需要注意的是,直接在编译器中运行程序是无法产生捕获异常的,只有在Debug、Release环境下运行,异常才会以对话框的形式直接弹出。
向函数中传递参数
现在我们来改变一下异步函数,让它接收一些参数。
- class Program
- {
- public delegate string DelegateWithParameters(int param1, string param2, ArrayList param3);
- static private string FuncWithParameters(int param1, string param2, ArrayList param3)
- {
- // 我们在这里改变参数值
- param1 = ;
- param2 = "hello";
- param3 = new ArrayList();
- return "thank you for reading me";
- }
- public static void Main(string[] args)
- {
- // 创建几个参数
- string strParam = "Param1";
- int intValue = ;
- ArrayList list = new ArrayList();
- list.Add("Item1");
- // 创建委托对象
- DelegateWithParameters delFoo =
- new DelegateWithParameters(FuncWithParameters);
- // 调用异步函数
- IAsyncResult tag =
- delFoo.BeginInvoke(intValue, strParam, list, null, null);
- // 通常调用线程会立即得到响应
- // 因此你可以在这里进行一些其他处理
- // 执行EndInvoke来取得返回值
- string strResult = delFoo.EndInvoke(tag);
- Console.WriteLine("param1: " + intValue);
- Console.WriteLine("param2: " + strParam);
- Console.WriteLine("ArrayList count: " + list.Count);
- Console.WriteLine("返回值: " + strResult);
- Console.ReadKey();
- }
- }
- //param1: 100
- //param2: Param1
- //ArrayList count: 1
- //返回值: thank you for reading me
我们的异步函数对参数的改变并没有影响其传出值,现在我们把ArrayList变为ref参数,看看会给EndInvoke带来什么变化。
- class Program
- {
- public delegate string DelegateWithParameters(out int param1, string param2, ref ArrayList param3);
- static private string FuncWithParameters(out int param1, string param2, ref ArrayList param3)
- {
- // 我们在这里改变参数值
- param1 = ;
- param2 = "hello";
- param3 = new ArrayList();
- return "thank you for reading me";
- }
- public static void Main(string[] args)
- {
- // 创建几个参数
- string strParam = "Param1";
- int intValue = ;
- ArrayList list = new ArrayList();
- list.Add("Item1");
- // 创建委托对象
- DelegateWithParameters delFoo =
- new DelegateWithParameters(FuncWithParameters);
- // 调用异步函数
- IAsyncResult tag =
- delFoo.BeginInvoke(out intValue, strParam, ref list, null, null);
- // 通常调用线程会立即得到响应
- // 因此你可以在这里进行一些其他处理
- // 执行EndInvoke来取得返回值
- string strResult = delFoo.EndInvoke(out intValue,ref list,tag);
- Console.WriteLine("param1: " + intValue);
- Console.WriteLine("param2: " + strParam);
- Console.WriteLine("ArrayList count: " + list.Count);
- Console.WriteLine("返回值: " + strResult);
- Console.ReadKey();
- }
- }
- //param1: 200
- //param2: Param1
- //ArrayList count: 0
- //返回值: thank you for reading me
param2没有变化,因为它是输入参数;param1作为输出参数,被更新为300;ArrayList的值已被重新分配,我们可以发现它的引用被指向了一个空元素的ArrayList对象(初始引用已丢失)。通过以上实例,我们应该能理解参数是如何在BeginInvoke与EndInvoke之间传递的。现在我们来尝试完成一个非阻塞模式下的异步调用,这是个重头戏!
C# 委托高级应用----线程——创建无阻塞的异步调用(一)的更多相关文章
- C# 委托高级应用----线程——创建无阻塞的异步调用(二)
了解IAsyncResult 现在我们已经了解,EndInvoke可以给我们提供传出参数与更新后的ref参数:也可以向我们导出异步函数中的异常信息.例如,我们使用BeginInvoke调用了异步函数S ...
- 谈.Net委托与线程——创建无阻塞的异步调用(一)
前言 本文大部分内容来自于mikeperetz的Asynchronous Method Invocation及本人的一些个人体会所得,希望对你有所帮助.原英文文献可以在codeproject中搜索到. ...
- 谈.Net委托与线程——创建无阻塞的异步调用(二)
了解IAsyncResult 现在我们已经了解,EndInvoke可以给我们提供传出参数与更新后的ref参数:也可以向我们导出异步函数中的异常信息.例如,我们使用BeginInvoke调用了异步函数S ...
- Python并发编程06 /阻塞、异步调用/同步调用、异步回调函数、线程queue、事件event、协程
Python并发编程06 /阻塞.异步调用/同步调用.异步回调函数.线程queue.事件event.协程 目录 Python并发编程06 /阻塞.异步调用/同步调用.异步回调函数.线程queue.事件 ...
- Unix 环境高级编程---线程创建、同步、
一下代码主要实现了linux下线程创建的基本方法,这些都是使用默认属性的.以后有机会再探讨自定义属性的情况.主要是为了练习三种基本的线程同步方法:互斥.读写锁以及条件变量. #include < ...
- 并发编程 - 线程 - 1.线程queue/2.线程池进程池/3.异步调用与回调机制
1.线程queue :会有锁 q=queue.Queue(3) q.get() q.put() 先进先出 队列后进先出 堆栈优先级队列 """先进先出 队列"& ...
- C# 创建线程的简单方式:异步委托 .
定义一个委托调用的方法:TakesAWhile //定义委托要引用的方法 private static int TakesAWhile(int data, int ms) { Console.Writ ...
- 理解Android线程创建流程(转)
/android/libcore/libart/src/main/java/java/lang/Thread.java /art/runtime/native/java_lang_Thread.cc ...
- 理解Android线程创建流程
copy from : http://gityuan.com/2016/09/24/android-thread/ 基于Android 6.0源码剖析,分析Android线程的创建过程 /androi ...
随机推荐
- 读lodash源码之从slice看稀疏数组与密集数组
卑鄙是卑鄙者的通行证,高尚是高尚者的墓志铭. --北岛<回答> 看北岛就是从这两句诗开始的,高尚者已死,只剩卑鄙者在世间横行. 本文为读 lodash 源码的第一篇,后续文章会更新到这个仓 ...
- SDRAM操作说明
SDRAM是做嵌入式系统中,常用是的缓存数据的器件.基本概念如下(注意区分几个主要常见存储器之间的差异): SDRAM(Synchronous Dynamic Random Access Memory ...
- 从cdn说起
为什么要使用cdn 雅虎军规有一条规则建议我们是用cdn.随便在网上搜索,可以找到使用的cdn的好处. 再次强调第一条黄金定律,减少网页内容的下载时间.提高下载速度还可以通过CDN(内容分发网络)来提 ...
- spring装配Bean过程
主要流程: 1.读取配置文件 2.实例化bean和填充bean属性 这个粗略的流程感觉更像是一个需求,有了这个需求,那么spring内部是怎么处理的呢? 我们知道spring的两个核心接口BeanFa ...
- Maven 整合 SSH 框架
前面的一系列文章中,我们总结了三大框架:Struts2,Hibernate,Spring 的基本知识.本篇就姑且尝试着使用 Maven 这个项目构建工具来将这三个框架整合一起.说到这里,如果有对 Ma ...
- webpack 3.X学习之基本配置
创建配置文件webpack.config.js 在根目录在手动创建webpack.config.js,配置基本模板 module.exports ={ entry:{}, output:{}, mod ...
- Linux指令 vi编辑,保存及退出
编辑模式 使用vi进入文本后,按i开始编辑文本退出编辑模式 按ESC键,然后: 退出vi :q! 不保存文件,强制退出vi命令 :w 保存文件,不退出vi命令 :wq 保存文件,退出vi命令 中断vi ...
- Nginx简介与安装
| 简介 Nginx是一个高性能的HTTP和反向代理服务器,可以作为负载均衡服务器,也是一个IMAP/POP3/SMTP服务器.它的特点是占有内存少,并发能力强.目前有很多大型公司都在使用Nginx, ...
- 2778:Ride to School-poj
2778:Ride to School 总时间限制: 1000ms 内存限制: 65536kB 描述 Many graduate students of Peking University are ...
- Unity3d_GUI_2__(能量条的学习)
这和上一篇有点跳跃,不过GUI有官方文档,而且也可以查看编辑器自带的脚本文档,多看看API,多操作操作,肯定能熟练的.所以这篇我就介绍下一点小技巧,大佬就略过这篇了,不适合大佬,会被你们教育的. 1. ...