关于异步的初步认识
在当初试用多线程的时候发现多线程能减轻或消除大量繁杂操作或过长等待时间造成的停滞感(就是线程阻塞)。后来发现使用异步操作也能达到相同的效果。但是两者之间是有区别的,之前在知识库里看了一些文章,我也记录了一下(有人云亦云的感觉),顺便也摆出一些个人观点。
多线程和异步虽然都可以减轻或消除线程阻塞而造成的停滞感,但是两者的本质上是有区别的
多线程是软件级别上的机制,在微观上它是分配CPU的时间片给某个进程中的各条线程,获得时间片的线程就可以处理它的任务,也就是执行代码。在其中负责调度CPU资源的就是操作系统,所以多线程是否能实现取决于操作系统,现今绝大部分操作系统都是多线程的系统,在DOS下是不支持多线程的。
异步则是硬件级别上的机制,在大学学习《计算机组成原理》时,就提过硬件的DMA(Direct Memory Access,直接内存存取),它是让一些计算机的外部设备(网卡,磁盘等)在不用消耗CPU时间的情况下,直接与内存交互进行数据读写,在此期间CPU可以着手其他事情,在IO完毕后才把调度权还给CPU。由此可见,过中不需要操作系统的支持,所以按照这样的思路去想,只需要计算机的硬件支持的话,在DOS中也能实现异步操作(这个在网上看到的,实际上我也没搞个DOS去实践)。
由上述的区别可以推断出他们的适用场合,异步是硬件层面的,一些关于硬件层面上的操作用起异步来会适合一些,例如文件的读写操作,数据库访问,网络访问等;多线程是软件层面的,CPU是否把时间片分配给当前线程,与能否让外部设备直接访问内存关系不大,相同的操作也是同样要消耗相同的CPU时间,相比起来,一些要CPU花费大量时间去处理的操作用多线程去实现会恰当一点。
理论上就如前面所说的,但是回到.NET Framework里面,貌似是另一回事了。通过运行以下代码
1 static void Main(string[] args) 2 { 3 PrintThreadInfo("Main"); 4 Action act = delegate() { PrintThreadInfo("Action"); }; 5 IAsyncResult ir = act.BeginInvoke(new AsyncCallback(AsyncCallback), act); 6 7 Console.ReadLine(); 8 } 9 10 static void AsyncCallback(IAsyncResult result) 11 { 12 PrintThreadInfo("Callback"); 13 Action caller = result.AsyncState as Action; 14 caller.EndInvoke(result); 15 } 16 17 static void PrintThreadInfo( string host ) 18 { 19 Console.WriteLine(string.Format( " {2} call: Current Thread Id is {0}, it {1} in threadpool",Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread?"is":"isn't",host )); 20 }
发现异步操作实际上还是利用了多线程,而且这条新开辟的线程是来源于线程池ThreadPool的。在MSDN上翻找了一下BeginInvoke的解释的确是异步调用的。它确确实实属于APM模式(BeginXXX/EndXXX)。那个再观察一下别的类在APM模式下的工作情况
1 static void Main(string[] args) 2 { 3 PrintThreadInfo("Main"); 4 byte[] datas = Encoding.ASCII.GetBytes("hello world"); 5 FileStream fs = new FileStream("abc.txt", FileMode.Create, FileAccess.Write,FileShare.Write, 1024, FileOptions.Asynchronous); 6 fs.BeginWrite(datas, 0, datas.Length, new System.AsyncCallback(AsyncCallback), fs); 7 8 9 Console.ReadLine(); 10 } 11 12 static void AsyncCallback(IAsyncResult result) 13 { 14 PrintThreadInfo("Callback"); 15 FileStream caller = result.AsyncState as FileStream; 16 caller.EndWrite(result); 17 caller.Close(); 18 caller.Dispose(); 19 }
这里选取了FileStream作例子,但从结果可以看出,纵使确确实实是异步操作,确确实实是文件写入,但是仍然是有调用了线程池,使用了线程。看回第一个例子的结果BeginInvoke和回调方法都是在同一条线程上执行的,相比起第二个例子就有个局限性,在BeginWrite调用的时候没办法看查看是否有使用线程去进行写操作,第二行信息是在回调时显示出来的。那这里是否和上一个例子一样两者都在同一个线程上运行呢?我有个比较拙劣的办法如下图所示
第一个例子中的情况
第二个例子中的情况
虽然这样断点测试貌似有点误差,不知有否说服力。对比之下还是可以看出第一个例子它调用异步的时候就创建了线程,严格意义上并不属于异步,第二个例子调用异步方法时没有创建线程,直到回传的时候才去创建了线程。可以初步证实在调用BeginWrite的时候并没有去创建线程,确实是使用了DMA机制,确确实实是异步调用了。
参考赵劼老师说的话,CLR会(通过Windows API)发出一个IRP(I/O Request Packet)。当设备准备妥当,就会找出一个它“最想处理”的IRP(例如一个读取离当前磁头最近的数据的请求)并进行处理,处理完毕后设备将会(通过Windows)交还一个表示工作完成的IRP。CLR会为每个进程创建一个IOCP(I/O Completion Port)并和Windows操作系统一起维护。IOCP中一旦被放入表示完成的IRP之后(通过内部的ThreadPool.BindHandle完成),CLR就会尽快分配一个可用的线程用于继续接下去的任务。
个人理解就是CLR与底层硬件交互,让相应设备的不再消耗CPU去设备访存。正如文章开头的理论部分所言,异步属于硬件方面的,所以一些委托的异步调用BeginInvoke并是假异步,例如上面文件操作的异步才是真异步,能实现真异步的有以下方法
- FileStream操作:BeginRead、BeginWrite(只有构造FileStream时传入FileOptions.Asynchronous参数才能获取真正的异步操作,否则仍然是假异步)。
- DNS操作:BeginGetHostByName、BeginResolve。
- Socket操作:BeginAccept、BeginConnect、BeginReceive等等。
- WebRequest操作:BeginGetRequestStream、BeginGetResponse。
- SqlCommand操作:BeginExecuteReader、BeginExecuteNonQuery等等(要在连接字符串中把Asynchronous Processing设为true,否则调用异步方法时会抛异常)。
- WebServcie调用操作:例如.NET 2.0或WCF生成的Web Service Proxy中的BeginXXX方法、WCF中ClientBase<TChannel>的InvokeAsync方法。
最后要记录一下的就是使用APM模式的时候一定要调用回调方法或者EndXXX,否则线程资源无法回收,有可能导致系统崩溃。
以上文章有参考赵劼老师的《正确使用异步操作》,还有一部分是在下的拙见,各位觉得在下有什么说错的欢迎批评指正,有什么建议或意见尽管说说。谢谢!
大话C#之委托
前言
开篇先来扯下淡,上篇博客LZ在结尾说这篇博客会来说说C#中的事件。但是当LZ看完事件之后发现事件是以委托为基础来实现的,于是LZ就自作主张地在这篇博客中先来说说委托,还烦请各位看官见谅!!!另外关于委托推荐一篇Jimmy Zhang写的关于委托的博客(C# 中的委托和事件),叙述非常有条理,可见子阳兄的文笔不凡。
博客结构
- 加工厂问题
- 委托来提高加工厂效率
- 委托的更多用法
- 委托到底是什么
加工厂问题
假设现在我们开了一个电子设备外包工厂(就像里面有很多人跳楼的那某某康),专门负责为国际上的大公司代工生产电子设备产品。某天,加工厂收到了来自美国苹果公司的订单,苹果公司委托加工厂为他们生产一批iPhone电话机,于是加工厂就像下面这么干,效果不错还挺好,苹果公司很满意。
1 static void Main(string[] args) 2 { 3 DigitalFactory factory = new DigitalFactory(); 4 factory.MakeiPhone(200); 5 } 6 7 //摘要: 8 // 加工厂类 9 public class DigitalFactory 10 { 11 // 生产iPhone 12 public void MakeiPhone(Int32 Number) 13 { 14 Console.WriteLine("" + Number + "台iPhone已经成功生产出来了......");
15 Console.ReadKey(); 16 } 17 } 18 输出: 19 200台iPhone已经成功生产出来了......
后来呢,苹果公司觉得加工厂生产的iPhone质量很好,于是又委托加工厂为他们生产一批iPad平板电脑,于是加工厂又像下面这么干了。
1 static void Main(string[] args) 2 { 3 DigitalFactory factory = new DigitalFactory(); 4 factory.MakeiPhone(200); 5 factory.MakeiPad(200); 6 } 7 8 //摘要: 9 // 加工厂类 10 public class DigitalFactory 11 { 12 // 生产iPhone 13 public void MakeiPhone(Int32 Number) 14 { 15 Console.WriteLine("" + Number + "台iPhone已经成功生产出来了......"); 16 Console.Read(); 17 } 18 19 // 生产iPad 20 public void MakeiPad(Int32 Number) 21 { 22 Console.WriteLine("" + Number + "台iPad已经成功生产出来了......"); 23 Console.Read(); 24 } 25 } 26 输出: 27 200台iPhone已经成功生产出来了...... 28 200台iPad已经成功生产出来了......
这次苹果公司更满意,又继续给了代工生产iMac和iPod的两个订单,于是加工厂又傻乎乎地像下面这么干了。
1 static void Main(string[] args) 2 { 3 DigitalFactory factory = new DigitalFactory(); 4 factory.MakeiPhone(200); 5 factory.MakeiPad(200); 6 factory.MakeiMac(200); 7 factory.MakeiPod(200); 8 } 9 10 //摘要: 11 // 加工厂类 12 public class DigitalFactory 13 { 14 // 生产iPhone 15 public void MakeiPhone(Int32 Number) 16 { 17 Console.WriteLine("" + Number + "台iPhone已经成功生产出来了......"); 18 Console.Read(); 19 } 20 21 // 生产iPad 22 public void MakeiPad(Int32 Number) 23 { 24 Console.WriteLine("" + Number + "台iPad已经成功生产出来了......"); 25 Console.Read(); 26 } 27 28 // 生产iMac 29 public void MakeiMac(Int32 Number) 30 { 31 Console.WriteLine("" + Number + "台iMac已经成功生产出来了......"); 32 Console.Read(); 33 } 34 35 // 生产iPod 36 public void MakeiPod(Int32 Number) 37 { 38 Console.WriteLine("" + Number + "台iPod已经成功生产出来了......"); 39 Console.Read(); 40 } 41 } 42 输出: 43 200台iPhone已经成功生产出来了...... 44 200台iPad已经成功生产出来了...... 45 200台iMac已经成功生产出来了...... 46 200台iPod已经成功生产出来了......
到现在加工厂才发现,随着订单越来越多,每代工生产一种产品就得新定义一个方法,那样效率太低了已经忙不过来了。所以,加工厂采用了一种新的生产模式来提高效率。
委托来提高加工厂效率
前面说到加工厂发现傻傻地为每一种代工产品提供一个发放的效率实在是太低下了,于是采用了一种新的模式。没错,大家可能都已经猜到了,这种新模式就是委托,让我们一起来看一下这新模式是如何提高效率的。
1 //摘要: 2 // 定义一个委托类型 3 public delegate void Dele(Int32 Number); 4 5 static void Main(string[] args) 6 { 7 DigitalFactory factory = new DigitalFactory(); 8 //将生产iPhone的方法传递给生产电子设备的方法 9 factory.MakeDigitals(new Apple().MakeiPhone, 200); 10 //将生产iPad的方法传递给生产电子设备的方法 11 factory.MakeDigitals(new Apple().MakeiPad, 200); 12 //将生产iMac的方法传递给生产电子设备的方法 13 factory.MakeDigitals(new Apple().MakeiMac, 200); 14 //将生产iPod的方法传递给生产电子设备的方法 15 factory.MakeDigitals(new Apple().MakeiPod, 200); 16 } 17 18 //摘要: 19 // 加工厂类 20 public class DigitalFactory 21 { 22 //定义一个通用的生产设备方法,接受一个委托变量和一个设备预生产数量作为参数 23 public void MakeDigitals(Dele dele, Int32 Number) 24 { 25 //判断委托对象是否为空,非空才执行 26 if (dele != null) 27 { 28 dele(Number); 29 } 30 } 31 } 32 33 //摘要: 34 // 苹果公司类 35 public class Apple 36 { 37 // 生产iPhone 38 public void MakeiPhone(Int32 Number) 39 { 40 Console.WriteLine("" + Number + "台iPhone已经成功生产出来了......"); 41 Console.Read(); 42 } 43 44 // 生产iPad 45 public void MakeiPad(Int32 Number) 46 { 47 Console.WriteLine("" + Number + "台iPad已经成功生产出来了......"); 48 Console.Read(); 49 } 50 51 // 生产iMac 52 public void MakeiMac(Int32 Number) 53 { 54 Console.WriteLine("" + Number + "台iMac已经成功生产出来了......"); 55 Console.Read(); 56 } 57 58 // 生产iPod 59 public void MakeiPod(Int32 Number) 60 { 61 Console.WriteLine("" + Number + "台iPod已经成功生产出来了......"); 62 Console.Read(); 63 } 64 } 65 输出: 66 200台iPhone已经成功生产出来了...... 67 200台iPad已经成功生产出来了...... 68 200台iMac已经成功生产出来了...... 69 200台iPod已经成功生产出来了......
加工厂现在定义了一个委托(上面代码中的Dele),要求苹果公司就是按照委托的要求将产品的设计、工艺、生产流程等和生产产品所有有关的细节全整理出来。加工厂现在就只有一个通用的产品生产方法(上面代码中的MakeDigitals),加工厂不再关心电子设备的其他细节,只要按照苹果公司给的生产设备的方法(上面代码中的MakeiPhone,MakeiPad等)生产出产品即可。这样,加工厂的效率是不是就提高多了?
委托往简单的方面说,就是“把方法作为方法的参数传递给方法”。这句话是不是很绕口?意思就是假如方法A接受一个委托类型的参数,其他只要符合委托类型签名的方法就都可以当做参数传递给方法A。在上面的代码中,委托Dele签名指定的方法要获取一个Int32类型的参数,并且返回值为void。苹果公司定义的四个方法MakeiPhone,MakeiPad,MakeiMac和MakeiPod的签名都符合委托类型Dele指定的方法的签名,所以这四个方法才能被传递给接受Dele类型作为参数的MakeDigitals方法。然后在MakieDigitals方法内部回调当做参数传递进来的方法,执行生产电子设备的逻辑。
让我们来看一看c#中委托的定义:
1 //摘要: 2 // 定义一个委托类型 3 public delegate void Dele(Int32 Number);
C#通过delegate关键字来声明委托,同时要指定委托的返回类型(此处为void)和参数(此处为Int32类型的Number),这样委托就声明好了。定义好的委托就相当于一个“类型”。下面就来看一下接受委托类型作为参数的方法的定义:
1 //定义一个通用的生产设备方法,接受一个委托变量和一个设备预生产数量作为参数 2 public void MakeDigitals(Dele dele, Int32 Number) 3 { 4 //判断委托对象是否为空,非空才执行 5 if (dele != null) 6 { 7 dele(Number); 8 } 9 }
MakeDigitals方法接受一个Dele类型的委托和一个32位整数作为参数(把Dele想象成String就很好理解了,委托让我们也过了一个定义“类型”的瘾,哈哈)。只要符合Dele指定签名的方法就都可以作为“参数”传递给MakeDigitals方法了。下面就是调用MakeDigitals方法的代码:
1 DigitalFactory factory = new DigitalFactory(); 2 //将生产iPhone的方法传递给生产电子设备的方法 3 factory.MakeDigitals(new Apple().MakeiPhone, 200);
看到这是不是觉得委托很简单呢?其实委托还可以帮助我们完成更多的事!
委托的更多用法
在加工厂的代码中我们只是使用委托回调了实例方法,其实委托还可以回调静态方法,还可以通过委托链一次性调用多个方法。下面我们就还是通过加工厂的代码来试探一下委托这位兄台都有哪些本事!
- 委托调用静态方法
加工厂的例子中我们全部回调的是实例方法,下面我们还是通过加工厂来调用一下静态方法:
1 //摘要: 2 // 定义一个委托类型 3 public delegate void Dele(Int32 Number); 4 5 static void Main(string[] args) 6 { 7 DigitalFactory factory = new DigitalFactory(); 8 //将生产iPhone的方法传递给生产电子设备的方法,此时MakeiPhone为静态方法 9 factory.MakeDigitals(Apple.MakeiPhone, 200); 10 } 11 12 //摘要: 13 // 加工厂类 14 public class DigitalFactory 15 { 16 //定义一个通用的生产设备方法,接受一个委托变量和一个设备预生产数量作为参数 17 public void MakeDigitals(Dele dele, Int32 Number) 18 { 19 //判断委托对象是否为空,非空才执行 20 if (dele != null) 21 { 22 dele(Number); 23 } 24 } 25 } 26 27 //摘要: 28 // 苹果公司类 29 public class Apple 30 { 31 // 生产iPhone,静态方法 32 public static void MakeiPhone(Int32 Number) 33 { 34 Console.WriteLine("" + Number + "台iPhone已经成功生产出来了......"); 35 Console.Read(); 36 } 37 } 38 输出: 39 200台iPhone已经成功生产出来了......
通过委托回调静态方法和回调实例方法类似,按照传统的静态方法调用方式调用就行了。
- 通过委托链调用多个方法
CLR通过委托链在很大程度上帮助我们减少了新建委托类型对象的数量,只要是符合委托类型签名规则的方法,都可以加入到委托类型实例的委托链中。CLR会在调用委托类型实例的代码处循环去调用委托链 中的方法,我们一起来看一下委托链的使用:
1 //省略Dele、Apple和DigitalFactory的定义代码 2 3 static void Main(string[] args) 4 { 5 DigitalFactory factory = new DigitalFactory(); 6 //新建一个Dele类型的委托对象,并且把MakeiPhone方法包装到委托对象里面 7 Dele del1 = new Dele(Apple.MakeiPhone); 8 //将MakeiPad方法加入委托对象的委托链 9 del1 += new Apple().MakeiPad; 10 //将MakeiMac方法加入委托对象的委托链 11 del1 += new Apple().MakeiMac; 12 //将MakeiPod方法加入委托对象的委托链 13 del1 += new Apple().MakeiPod; 14 factory.MakeDigitals(del1, 200); 15 } 16 输出: 17 200台iPhone已经成功生产出来了...... 18 200台iPad已经成功生产出来了...... 19 200台iMac已经成功生产出来了...... 20 200台iPod已经成功生产出来了......
可以看到在上面的代码中,我们新建了一个Dele类型的委托对象(新建时包装了对Apple.MakeiPhone方法的引用),然后通过 += 操作符将其余三个方法的引用加入到了del1的委托链中,最后通过和加工厂代码相同的方式调用委托,输出了同样的结果。
- 委托与Lambda表达式
将符合委托类型方法签名的方法定义好,然后再通过委托回调这些方法固然很有用,绝大多数时候我们也这么干,效果也还不错。但是在某些情况下定义的方法就被回调一两次,这太对不起我们辛辛苦苦地定 义方法了,Microsoft于是急我们之所急为我们提供了简便方法,真是大好人呐!!!下面我们来看看大好人是怎么对我们好的:
1 //省略Dele、Apple和DigitalFactory的定义代码 2 3 static void Main(string[] args) 4 { 5 DigitalFactory factory = new DigitalFactory(); 6 factory.MakeDigitals(obj => 7 { 8 Console.WriteLine(obj + "台iPhone已经成功生产出来了......"); 9 Console.ReadKey(); 10 }, 200); 11 } 12 输出: 13 200台iPhone已经成功生产出来了......
上面我们就没定义MakeiPhone()方法,把它的实现内联进了代码里面。=>的左边就是方法需要的参数,如果方法需要多个参数就需要用括号括起来并且用逗号隔开。=>右边是方法的主体,多行语句也需要用大括号括起来。如果委托预期一个返回值,直接在内联代码里面加入return语句就行了。
另外,在使用Lambda表达式内联进代码的方式时,Lambda表达式还可以访问类的内部成员,像下面这样:
1 //省略Dele、Apple和DigitalFactory的定义代码 2 3 static void Main(string[] args) 4 { 5 String LambdaDesc = "这是使用Lambda表达式实现的委托调用:"; 6 DigitalFactory factory = new DigitalFactory(); 7 factory.MakeDigitals(obj => 8 { 9 Console.Write(LambdaDesc); 10 Console.WriteLine(obj + "台iPhone已经成功生产出来了......"); 11 Console.ReadKey(); 12 }, 200); 13 } 14 输出: 15 这是使用Lambda表达式实现的委托调用:200台iPhone已经成功生产出来了......
在Lambda表达式的内部,我们访问了属于Main方法的LambdaDesc变量。值得注意的是此时的MakeDigitals是实例方法,如果MakeDigitals是静态方法那么就只能访问静态成员,而不能访问此处的LambdaDesc。
另外:FCL已经为我们定义了大部分一般情况下需要的委托类型,例如:System.Func<out TResult>、System.Predicate<in T>、System.Action<>等等。这些委托类型能够满足绝大部分我们日常编码中的需要。
委托到底是什么
前面我们看过了委托的各种用法,那么委托为什么能够回调方法?委托到底是怎么实现的呢?下面我们就来看看委托究竟是个什么东西!
首先,我们使用ILDasm.exe来查看生成的程序集,看看编译器生成了什么IL,编译器生成的IL如下:
通过上面的图片我们看到,编译器把Dele类型的委托编译成了一个类,这个类继承自System.MulticastDelete,同时它还有一个无返回值的构造函数,以及BeginInvoke、EndInvoke和Invoke三个方法。到这里就真相大白了,其实委托是一个类。它的完整定义就像下面这样:
1 public class Dele : System.MulticastDelegate 2 { 3 //构造器 4 public Dele(Object object,IntPtr method); 5 6 public virtual void Invoke(Int32 value); 7 8 //以下两个方法实现了对方法的回调 9 public virtual IAsyncResult BeginInvoke(Int32 value,AsyncCallback callback,Object object); 10 11 public virtual IAsyncResult EndInvoke(IAsyncResult result); 12 }
所有的委托类型都派生自System.MulticastDelegate,System.MulticastDelegate又派生自System.Delegate,而System.Delegate最终派生自所有类型的基类:System.Object。因为委托是类,所以只要能够定义类的地方就都可以定义委托,下面我们先来看一下System.MulticastDelegate中三个非常重要的非公共字段_target、_methodPtr和_invocationList:
- _target:从定义就可以看出,这个字段引用的是回调方法要操作的对象,也就是定义回调方法的类型的实例,在加工厂的代码里面,_target引用的就是Apple的实例。值得注意的是如果委托对象包装的是一个静态方法,那么_target就为null。
- _methodPtr:代表的是一个整数值,CLR用这个整数值来标识要回调的方法。
- _invocationList:引用的是一个委托数组,当委托对象只包装了一个方法时,该字段的值为null。如果委托对象包装了一组方法,该字段就引用一个委托数组,就是我们前面使用的委托链的实现。
因为上面的三个字段都是MulticastDelegate类的非公共字段,所以是不能访问的,但是我们可以访问Delegate的Target和Method属性,功能和_target以及_methodPtr一样。我们还是通过加工厂的代码来看一下:
1 public class DigitalFactory 2 { 3 //定义一个通用的生产设备方法,接受一个委托变量和一个设备预生产数量作为参数 4 public void MakeDigitals(Dele dele, Int32 Number) 5 { 6 //判断委托对象是否为空,非空才执行 7 if (dele != null) 8 { 9 dele.Invoke(Number); 10 Console.WriteLine("Target:" + dele.Target + ",Method:" + dele.Method.Name); 11 Console.ReadKey(); 12 } 13 } 14 } 15 输出: 16 200台iPhone已经成功生产出来了...... 17 Target:AllChapters.Program+Apple,Method:MakeiPhone
上面的代码中,我们在MakeDigitals方法里访问了dele的Target属性和Method属性,并且成功把信息打印了出来。调用这两个属性来判断委托对象包装的方法所在的类型和包装的方法在某些时候非常有用!!!
总结
怎么样?现在对委托的用法和原理是不是有了一个重新的认识,在日常的代码中委托确实是一个非常有用的利器,对简化代码和提高代码重用率都有非常大的帮助。
最后希望我的文章能够对你有所帮助,如果你觉得文章还不错,麻烦请在右下角点一个推荐,谢谢!!!