在当初试用多线程的时候发现多线程能减轻或消除大量繁杂操作或过长等待时间造成的停滞感(就是线程阻塞)。后来发现使用异步操作也能达到相同的效果。但是两者之间是有区别的,之前在知识库里看了一些文章,我也记录了一下(有人云亦云的感觉),顺便也摆出一些个人观点。
多线程和异步虽然都可以减轻或消除线程阻塞而造成的停滞感,但是两者的本质上是有区别的
多线程是软件级别上的机制,在微观上它是分配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,否则线程资源无法回收,有可能导致系统崩溃。
以上文章有参考赵劼老师的《正确使用异步操作》,还有一部分是在下的拙见,各位觉得在下有什么说错的欢迎批评指正,有什么建议或意见尽管说说。谢谢!