线程池(ThreadPool)

作者:编程技术

2.BackgroundWorker 

BackgroundWorker本质上是使用线程池内工作者线程,不过这个类已经多余了(了解即可)。在BackgroundWorkerDoWork属性追加自定义方法,通过RunWorkerAsync将自定义方法追加进池化线程内处理。

DoWork本质上是一个事件(event)。委托类型限制为无返回值且参数有两个分别为Object和DoWorkEventArgs类型。

1 public event DoWorkEventHandler DoWork;
2 
3 public delegate void DoWorkEventHandler(object sender, DoWorkEventArgs e);

示例如下:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             int workerThreadsCount, completionPortThreadsCount;
 6             ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
 7             Console.WriteLine("剩余工作线程数:{0},剩余IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
 8             {
 9                 BackgroundWorker backgroundWorker = new BackgroundWorker();
10                 backgroundWorker.DoWork  = DoWork;
11                 backgroundWorker.RunWorkerAsync();
12             }
13             Thread.Sleep(1000);
14             ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
15             Console.WriteLine("剩余工作线程数:{0},剩余IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
16             Console.ReadKey();
17         }
18         private static void DoWork(object sender, DoWorkEventArgs e)
19         {
20             Thread.Sleep(2000);
21             Console.WriteLine("demo-ok");
22         }
23     }

内部占用线程内线程,结果如下:

图片 1

 

 static void Main(string[] args)

异步编程:使用线程池管理线程,异步线程

异步编程:使用线程池管理线程

图片 2

 从此图中我们会发现 .NET 与C# 的每个版本发布都是有一个“主题”。即:C#1.0托管代码→C#2.0泛型→C#3.0LINQ→C#4.0动态语言→C#5.0异步编程。现在我为最新版本的“异步编程”主题写系列分享,期待你的查看及点评。

 

如今的应用程序越来越复杂,我们常常需要使用《异步编程:线程概述及使用》中提到的多线程技术来提高应用程序的响应速度。这时我们频繁的创建和销毁线程来让应用程序快速响应操作,这频繁的创建和销毁无疑会降低应用程序性能,我们可以引入缓存机制解决这个问题,此缓存机制需要解决如:缓存的大小问题、排队执行任务、调度空闲线程、按需创建新线程及销毁多余空闲线程……如今微软已经为我们提供了现成的缓存机制:线程池

         线程池原自于对象池,在详细解说明线程池前让我们先来了解下何为对象池。

流程图:

 图片 3

 

         对于对象池的清理通常设计两种方式:

1)         手动清理,即主动调用清理的方法。

2)         自动清理,即通过System.Threading.Timer来实现定时清理。

 

关键实现代码:

 

  图片 4public sealed class ObjectPool<T> where T : ICacheObjectProxy<T> { // 最大容量 private Int32 m_maxPoolCount = 30; // 最小容量 private Int32 m_minPoolCount = 5; // 已存容量 private Int32 m_currentCount; // 空闲 被用 对象列表 private Hashtable m_listObjects; // 最大空闲时间 private int maxIdleTime = 120; // 定时清理对象池对象 private Timer timer = null; /// <summary> /// 创建对象池 /// </summary> /// <param name="maxPoolCount">最小容量</param> /// <param name="minPoolCount">最大容量</param> /// <param name="create_params">待创建的实际对象的参数</param> public ObjectPool(Int32 maxPoolCount, Int32 minPoolCount, Object[] create_params){ } /// <summary> /// 获取一个对象实例 /// </summary> /// <returns>返回内部实际对象,若返回null则线程池已满</returns> public T GetOne(){ } /// <summary> /// 释放该对象池 /// </summary> public void Dispose(){ } /// <summary> /// 将对象池中指定的对象重置并设置为空闲状态 /// </summary> public void ReturnOne(T obj){ } /// <summary> /// 手动清理对象池 /// </summary> public void ManualReleaseObject(){ } /// <summary> /// 自动清理对象池(对大于 最小容量 的空闲对象进行释放) /// </summary> private void AutoReleaseObject(Object obj){ } } 实现的关键代码

 

通过对“对象池”的一个大体认识能帮我们更快理解线程池。

 

线程池ThreadPool类详解

ThreadPool静态类,为应用程序提供一个由系统管理的辅助线程池,从而使您可以集中精力于应用程序任务而不是线程管理。每个进程都有一个线程池,一个Process中只能有一个实例,它在各个应用程序域(AppDomain)是共享的。

在内部,线程池将自己的线程划分工作者线程(辅助线程)和I/O线程。前者用于执行普通的操作,后者专用于异步IO,比如文件和网络请求,注意,分类并不说明两种线程本身有差别,内部依然是一样的。

图片 5public static class ThreadPool { // 将操作系统句柄绑定到System.Threading.ThreadPool。 public static bool BindHandle(SafeHandle osHandle); // 检索由ThreadPool.GetMaxThreads(Int32,Int32)方法返回的最大线程池线程数和当前活动线程数之间的差值。 public static void GetAvailableThreads(out int workerThreads , out int completionPortThreads); // 设置和检索可以同时处于活动状态的线程池请求的数目。 // 所有大于此数目的请求将保持排队状态,直到线程池线程变为可用。 public static bool SetMaxThreads(int workerThreads, int completionPortThreads); public static void GetMaxThreads(out int workerThreads, out int completionPortThreads); // 设置和检索线程池在新请求预测中维护的空闲线程数。 public static bool SetMinThreads(int workerThreads, int completionPortThreads); public static void GetMinThreads(out int workerThreads, out int completionPortThreads); // 将方法排入队列以便执行,并指定包含该方法所用数据的对象。此方法在有线程池线程变得可用时执行。 public static bool QueueUserWorkItem(WaitCallback callBack, object state); // 将重叠的 I/O 操作排队以便执行。如果成功地将此操作排队到 I/O 完成端口,则为 true;否则为 false。 // 参数overlapped:要排队的System.Threading.NativeOverlapped结构。 public static bool UnsafeQueueNativeOverlapped(NativeOverlapped* overlapped); // 将指定的委托排队到线程池,但不会将调用堆栈传播到工作者线程。 public static bool UnsafeQueueUserWorkItem(WaitCallback callBack, object state); // 注册一个等待Threading.WaitHandle的委托,并指定一个 32 位有符号整数来表示超时值(以毫秒为单位)。 // executeOnlyOnce如果为 true,表示在调用了委托后,线程将不再在waitObject参数上等待; // 如果为 false,表示每次完成等待操作后都重置计时器,直到注销等待。 public static RegisteredWaitHandle RegisterWaitForSingleObject( WaitHandle waitObject , WaitOrTimerCallback callBack, object state, Int millisecondsTimeOutInterval, bool executeOnlyOnce); public static RegisteredWaitHandle UnsafeRegisterWaitForSingleObject( WaitHandle waitObject , WaitOrTimerCallback callBack , object state , int millisecondsTimeOutInterval , bool executeOnlyOnce); …… } ThreadPool

1)         使用GetMaxThreads()和SetMaxThreads()获取和设置最大线程数

可排队到线程池的操作数仅受内存的限制;而线程池限制进程中可以同时处于活动状态的线程数(默认情况下,限制每个 CPU 可以使用 25 个工作者线程和 1,000 个 I/O 线程(根据机器CPU个数和.net framework版本的不同,这些数据可能会有变化)),所有大于此数目的请求将保持排队状态,直到线程池线程变为可用。

不建议更改线程池中的最大线程数:

a)         将线程池大小设置得太大,可能会造成更频繁的执行上下文切换及加剧资源的争用情况。

b)         其实FileStream的异步读写,异步发送接受Web请求,System.Threading.Timer定时器,甚至使用delegate的beginInvoke都会默认调用 ThreadPool,也就是说不仅你的代码可能使用到线程池,框架内部也可能使用到。

c)         一个应用程序池是一个独立的进程,拥有一个线程池,应用程序池中可以有多个WebApplication,每个运行在一个单独的AppDomain中,这些WebApplication公用一个线程池。

 

2)         使用GetMinThreads()和SetMinThreads()获取和设置最小空闲线程数

为避免向线程分配不必要的堆栈空间,线程池按照一定的时间间隔创建新的空闲线程(该间隔为半秒)。所以如果最小空闲线程数设置的过小,在短期内执行大量任务会因为创建新空闲线程的内置延迟导致性能瓶颈。最小空闲线程数默认值等于机器上的CPU核数,并且不建议更改最小空闲线程数。

在启动线程池时,线程池具有一个内置延迟,用于启用最小空闲线程数,以提高应用程序的吞吐量。

在线程池运行中,对于执行完任务的线程池线程,不会立即销毁,而是返回到线程池,线程池会维护最小的空闲线程数(即使应用程序所有线程都是空闲状态),以便队列任务可以立即启动。超过此最小数目的空闲线程一段时间没事做后会自己醒来终止自己,以节省系统资源。

3)         静态方法GetAvailableThreads()

通过静态方法GetAvailableThreads()返回的线程池线程的最大数目和当前活动数目之间的差值,即获取线程池中当前可用的线程数目

4)         两个参数

方法GetMaxThreads()、SetMaxThreads()、GetMinThreads()、SetMinThreads()、GetAvailableThreads()钧包含两个参数。参数workerThreads指工作者线程;参数completionPortThreads指异步 I/O 线程。

通过调用 ThreadPool.QueueUserWorkItem 并传递 WaitCallback 委托来使用线程池。也可以通过使用 ThreadPool.RegisterWaitForSingleObject 并传递 WaitHandle(在向其发出信号或超时时,它将引发对由 WaitOrTimerCallback 委托包装的方法的调用)来将与等待操作相关的工作项排队到线程池中。若要取消等待操作(即不再执行WaitOrTimerCallback委托),可调用RegisterWaitForSingleObject()方法返回的RegisteredWaitHandle的 Unregister 方法。

如果您知道调用方的堆栈与在排队任务执行期间执行的所有安全检查不相关,则还可以使用不安全的方法 ThreadPool.UnsafeQueueUserWorkItem 和 ThreadPool.UnsafeRegisterWaitForSingleObject。QueueUserWorkItem 和 RegisterWaitForSingleObject 都会捕获调用方的堆栈,此堆栈将在线程池线程开始执行任务时合并到线程池线程的堆栈中。如果需要进行安全检查,则必须检查整个堆栈,但它还具有一定的性能开销。使用“不安全的”方法调用并不会提供绝对的安全,但它会提供更好的性能。

让一个线程不确定地等待一个内核对象进入可用状态,这对线程的内存资源来说是一种浪费。ThreadPool.RegisterWaitForSingleObject()为我们提供了一种方式:在一个内核对象变得可用的时候调用一个方法。

使用需注意:

1)         WaitOrTimerCallback委托参数,该委托接受一个名为timeOut的Boolean参数。如果 WaitHandle 在指定时间内没有收到信号(即,超时),则为true,否则为 false。回调方法可以根据timeOut的值来针对性地采取措施。

2)         名为executeOnlyOnce的Boolean参数。传true则表示线程池线程只执行回调方法一次;若传false则表示内核对象每次收到信号,线程池线程都会执行回调方法。等待一个AutoResetEvent对象时,这个功能尤其有用。

3)         RegisterWaitForSingleObject()方法返回一个RegisteredWaitHandle对象的引用。这个对象标识了线程池正在它上面等待的内核对象。我们可以调用它的Unregister(WaitHandle waitObject)方法取消由RegisterWaitForSingleObject()注册的等待操作(即WaitOrTimerCallback委托不再执行)。Unregister(WaitHandle waitObject)的WaitHandle参数表示成功取消注册的等待操作后线程池会向此对象发出信号(set()),若不想收到此通知可以传递null。

         示例:

图片 6private static void Example_RegisterWaitForSingleObject() { // 加endWaitHandle的原因:如果执行过快退出方法会导致一些东西被释放,造成排队的任务不能执行,原因还在研究 AutoResetEvent endWaitHandle = new AutoResetEvent(false); AutoResetEvent notificWaitHandle = new AutoResetEvent(false); AutoResetEvent waitHandle = new AutoResetEvent(false); RegisteredWaitHandle registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject( waitHandle, (Object state, bool timedOut) => { if (timedOut) Console.WriteLine("RegisterWaitForSingleObject因超时而执行"); else Console.WriteLine("RegisterWaitForSingleObject收到WaitHandle信号"); }, null, TimeSpan.FromSeconds(2), true ); // 取消等待操作(即不再执行WaitOrTimerCallback委托) registeredWaitHandle.Unregister(notificWaitHandle); // 通知 ThreadPool.RegisterWaitForSingleObject( notificWaitHandle, (Object state, bool timedOut) => { if (timedOut) Console.WriteLine("第一个RegisterWaitForSingleObject没有调用Unregister()"); else Console.WriteLine("第一个RegisterWaitForSingleObject调用了Unregister()"); endWaitHandle.Set(); }, null, TimeSpan.FromSeconds(4), true ); endWaitHandle.WaitOne(); } 示例

执行上下文

         上一小节中说到:线程池最大线程数设置过大可能会造成Windows频繁执行上下文切换,降低程序性能。对于大多数园友不会满意这样的回答,我和你一样也喜欢“知其然,再知其所以然”。

.NET中上下文太多,我最后得出的结论是:上下文切换中的上下文专指“执行上下文”。

执行上下文包括:安全上下文、同步上下文(System.Threading.SynchronizationContext)、逻辑调用上下文(System.Runtime.Messaging.CallContext)。即:安全设置(压缩栈、Thread的Principal属性和Windows身份)、宿主设置(System.Threading.HostExcecutingContextManager)以及逻辑调用上下文数据(System.Runtime.Messaging.CallContext的LogicalSetData()和LogicalGetData()方法)。

当一个“时间片”结束时,如果Windows决定再次调度同一个线程,那么Windows不会执行上下文切换。如果Windows调度了一个不同的线程,这时Windows执行线程上下文切换。

         当Windows上下文切换到另一个线程时,CPU将执行一个不同的线程,而之前线程的代码和数据还在CPU的高速缓存中,(高速缓存使CPU不必经常访问RAM,RAM的速度比CPU高速缓存慢得多),当Windows上下文切换到一个新线程时,这个新线程极有可能要执行不同的代码并访问不同的数据,这些代码和数据不在CPU的高速缓存中。因此,CPU必须访问RAM来填充它的高速缓存,以恢复高速执行状态。但是,在其“时间片”执行完后,一次新的线程上下文切换又发生了。

上下文切换所产生的开销不会换来任何内存和性能上的收益。执行上下文所需的时间取决于CPU架构和速度(即“时间片”的分配)。而填充CPU缓存所需的时间取决于系统运行的应用程序、CPU、缓存的大小以及其他各种因素。所以,无法为每一次线程上下文切换的时间开销给出一个确定的值,甚至无法给出一个估计的值。唯一确定的是,如果要构建高性能的应用程序和组件,就应该尽可能避免线程上下文切换。

除此之外,执行垃圾回收时,CLR必须挂起(暂停)所有线程,遍历它们的栈来查找根以便对堆中的对象进行标记,再次遍历它们的栈(有的对象在压缩期间发生了移动,所以要更新它们的根),再恢复所有线程。所以,减少线程的数量也会显著提升垃圾回收器的性能。每次使用一个调试器并遇到一个断点,Windows都会挂起正在调试的应用程序中的所有线程,并在单步执行或运行应用程序时恢复所有线程。因此,你用的线程越多,调试体验也就越差。

Windows实际记录了每个线程被上下文切换到的次数。可以使用像Microsoft Spy 这样的工具查看这个数据。这个工具是Visual Studio附带的一个小工具(vs按安装路径Visual Studio 2012Common7Tools),如图

图片 7

在《异步编程:线程概述及使用》中我提到了Thread的两个上下文,即:

1)         CurrentContext        获取线程正在其中执行的当前上下文。主要用于线程内部存储数据。

2)         ExecutionContext    获取一个System.Threading.ExecutionContext对象,该对象包含有关当前线程的各种上下文的信息。主要用于线程间数据共享。

其中获取到的System.Threading.ExecutionContext就是本小节要说的“执行上下文”。

图片 8public sealed class ExecutionContext : IDisposable, ISerializable { public void Dispose(); public void GetObjectData(SerializationInfo info, StreamingContext context); // 此方法对于将执行上下文从一个线程传播到另一个线程非常有用。 public ExecutionContext CreateCopy(); // 从当前线程捕获执行上下文的一个副本。 public static ExecutionContext Capture(); // 在当前线程上的指定执行上下文中运行某个方法。 public static void Run(ExecutionContext executionContext, ContextCallback callback, object state); // 取消执行上下文在异步线程之间的流动。 public static AsyncFlowControl SuppressFlow(); public static bool IsFlowSuppressed(); // RestoreFlow 撤消以前的 SuppressFlow 方法调用的影响。 // 此方法由 SuppressFlow 方法返回的 AsyncFlowControl 结构的 Undo 方法调用。 // 应使用 Undo 方法(而不是 RestoreFlow 方法)恢复执行上下文的流动。 public static void RestoreFlow(); } View Code

ExecutionContext 类提供的功能让用户代码可以在用户定义的异步点之间捕获和传输此上下文。公共语言运行时(CLR)确保在托管进程内运行时定义的异步点之间一致地传输 ExecutionContext。

每当一个线程(初始线程)使用另一个线程(辅助线程)执行任务时,CLR会将前者的执行上下文流向(复制到)辅助线程(注意这个自动流向是单方向的)。这就确保了辅助线程执行的任何操作使用的是相同的安全设置和宿主设置。还确保了初始线程的逻辑调用上下文可以在辅助线程中使用。

但执行上下文的复制会造成一定的性能影响。因为执行上下文中包含大量信息,而收集所有这些信息,再把它们复制到辅助线程,要耗费不少时间。如果辅助线程又采用了更多地辅助线程,还必须创建和初始化更多的执行上下文数据结构。

所以,为了提升应用程序性能,我们可以阻止执行上下文的流动。当然这只有在辅助线程不需要或者不访问上下文信息的时候才能进行阻止。

下面给出一个示例为了演示:

1)         在线程间共享逻辑调用上下文数据(CallContext)。

2)         为了提升性能,阻止恢复执行上下文的流动。

3)         在当前线程上的指定执行上下文中运行某个方法。

图片 9private static void Example_ExecutionContext() { CallContext.LogicalSetData("Name", "小红"); Console.WriteLine("主线程中Name为:{0}", CallContext.LogicalGetData("Name")); // 1) 在线程间共享逻辑调用上下文数据(CallContext)。 Console.WriteLine("1)在线程间共享逻辑调用上下文数据(CallContext)。"); ThreadPool.QueueUserWorkItem((Object obj) => Console.WriteLine("ThreadPool线程中Name为:"{0}"", CallContext.LogicalGetData("Name"))); Thread.Sleep(500); Console.WriteLine(); // 2) 为了提升性能,取消恢复执行上下文的流动。 ThreadPool.UnsafeQueueUserWorkItem((Object obj) => Console.WriteLine("ThreadPool线程使用Unsafe异步执行方法来取消执行上下文的流动。Name为:"{0}"" , CallContext.LogicalGetData("Name")), null); Console.WriteLine("2)为了提升性能,取消/恢复执行上下文的流动。"); AsyncFlowControl flowControl = ExecutionContext.SuppressFlow(); ThreadPool.QueueUserWorkItem((Object obj) => Console.WriteLine("(取消ExecutionContext流动)ThreadPool线程中Name为:"{0}"", CallContext.LogicalGetData("Name"))); Thread.Sleep(500); // 恢复不推荐使用ExecutionContext.RestoreFlow() flowControl.Undo(); ThreadPool.QueueUserWorkItem((Object obj) => Console.WriteLine("(恢复ExecutionContext流动)ThreadPool线程中Name为:"{0}"", CallContext.LogicalGetData("Name"))); Thread.Sleep(500); Console.WriteLine(); // 3) 在当前线程上的指定执行上下文中运行某个方法。(通过获取调用上下文数据验证) Console.WriteLine("3)在当前线程上的指定执行上下文中运行某个方法。(通过获取调用上下文数据验证)"); ExecutionContext curExecutionContext = ExecutionContext.Capture(); ExecutionContext.SuppressFlow(); ThreadPool.QueueUserWorkItem( (Object obj) => { ExecutionContext innerExecutionContext = obj as ExecutionContext; ExecutionContext.Run(innerExecutionContext, (Object state) => Console.WriteLine("ThreadPool线程中Name为:"{0}""<br> , CallContext.LogicalGetData("Name")), null); } , curExecutionContext ); } View Code

结果如图:

图片 10

 

 

 注意:

1)         示例中“在当前线程上的指定执行上下文中运行某个方法”:代码中必须使用ExecutionContext.Capture()获取当前执行上下文的一个副本

a)         若直接使用Thread.CurrentThread.ExecutionContext则会报“无法应用以下上下文: 跨 AppDomains 封送的上下文、不是通过捕获操作获取的上下文或已作为 Set 调用的参数的上下文。”错误。

b)         若使用Thread.CurrentThread.ExecutionContext.CreateCopy()会报“只能复制新近捕获(ExecutionContext.Capture())的上下文”。

2)         取消执行上下文流动除了使用ExecutionContext.SuppressFlow()方式外。还可以通过使用ThreadPool的UnsafeQueueUserWorkItem 和 UnsafeRegisterWaitForSingleObject来执行委托方法。原因是不安全的线程池操作不会传输压缩堆栈。每当压缩堆栈流动时,托管的主体、同步、区域设置和用户上下文也随之流动。

 

线程池线程中的异常

线程池线程中未处理的异常将终止进程。以下为此规则的三种例外情况: 

  1. 由于调用了 Abort,线程池线程中将引发ThreadAbortException。 
    2. 由于正在卸载应用程序域,线程池线程中将引发AppDomainUnloadedException。 
  2. 公共语言运行库或宿主进程将终止线程。

何时不使用线程池线程

现在大家都已经知道线程池为我们提供了方便的异步API及托管的线程管理。那么是不是任何时候都应该使用线程池线程呢?当然不是,我们还是需要“因地制宜”的,在以下几种情况下,适合于创建并管理自己的线程而不是使用线程池线程:

 

 

  本博文介绍线程池以及其基础对象池,ThreadPool类的使用及注意事项,如何排队工作项到线程池,执行上下文及线程上下文传递问题…… 

线程池虽然为我们提供了异步操作的便利,但是它不支持对线程池中单个线程的复杂控制致使我们有些情况下会直接使用Thread。并且它对“等待”操作、“取消”操作、“延续”任务等操作比较繁琐,可能迫使你从新造轮子。微软也想到了,所以在.NET4.0的时候加入了“并行任务”并在.NET4.5中对其进行改进,想了解“并行任务”的园友可以先看看《(译)关于Async与Await的FAQ》。

本节到此结束,感谢大家的观赏。赞的话还请多推荐啊 (*^_^*)

 

 

 

 

参考资料:《CLR via C#(第三版)》

 

 摘自:

 

异步编程:使用线程池管理线程 从此图中我们会发现 .NET 与C# 的每个版本发布都是有一个主题...

线程池概述

由系统维护的容纳线程的容器,由CLR控制的所有AppDomain共享。线程池可用于执行任务、发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器。

 

 现在您意识到线程数量意味着什么了没?没错,就是我们刚才提到的“运算能力”。很多时候我们可以简单的认为,在同样的环境下,一个任务使用的线程数量越多,它所获得的运算能力就比另一个线程数量较少的任务要来得多。运算能力自然就涉及到任务执行的快慢。您可以设想一下,有一个生产任务,和一个消费任务,它们使用一个队列做临时存储。在理想情况下,生产和消费的速度应该保持相同,这样可以带来最好的吞吐量。如果生产任务执行较快,则队列中便会产生堆积,反之消费任务就会不断等待,吞吐量也会下降。因此,在实现的时候,我们往往会为生产任务和消费任务分别指派独立的线程池,并且通过增加或减少线程池内线程数量来条件运算能力,使生产和消费的步调达到平衡。

三种异步模式(扫盲)&BackgroundWorker 

 我们也可以使用代码来操作IO线程池,例如下面这个接口便是向IO线程池递交一个任务:

基础线程池&工作者线程(ThreadPool)

.NET中使用线程池用到ThreadPool类,ThreadPool是一个静态类,定义于System.Threading命名空间,自.NET 1.1起引入。

调用方法QueueUserWorkItem可以将一个异步的计算限制操作放到线程池的队列中,这个方法向线程池的队列添加一个工作项以及可选的状态数据。
工作项:由callBack参数标识的一个方法,该方法由线程池线程调用。可向方法传递一个state实参(多于一个参数则需要封装为实体类)。

1  public static bool QueueUserWorkItem(WaitCallback callBack);
2  public static bool QueueUserWorkItem(WaitCallback callBack, object state);

 下面是通过QueueUserWorkItem启动工作者线程的示例:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //方式一
 6             {
 7                 ThreadPool.QueueUserWorkItem(n => Test("Test-ok"));
 8             }
 9             //方式二
10             {
11                 WaitCallback waitCallback = new WaitCallback(Test);
12                 ThreadPool.QueueUserWorkItem(n => waitCallback("WaitCallback"));//两者效果相同 ThreadPool.QueueUserWorkItem(waitCallback,"Test-ok");
13             }
14             //方式三
15             {
16                 ParameterizedThreadStart parameterizedThreadStart = new ParameterizedThreadStart(Test);
17                 ThreadPool.QueueUserWorkItem(n => parameterizedThreadStart("ParameterizedThreadStart"));
18             }
19             //方式四
20             {
21                 TimerCallback timerCallback = new TimerCallback(Test);
22                 ThreadPool.QueueUserWorkItem(n => timerCallback("TimerCallback"));
23             }
24             //方式五
25             {
26                 Action<object> action = Test;
27                 ThreadPool.QueueUserWorkItem(n => Test("Action"));
28             }
29             //方式六
30             ThreadPool.QueueUserWorkItem((o) =>
31             {
32                 var msg = "lambda";
33                 Console.WriteLine("执行方法:{0}", msg);
34             });
35             
36             ......
37 
38             Console.ReadKey();
39         }
40         static void Test(object o)
41         {
42             Console.WriteLine("执行方法:{0}", o);
43         }
44         /*
45          * 作者:Jonins
46          * 出处:http://www.cnblogs.com/jonins/
47          */
48     }

执行结果如下:

图片 11

以上是使用线程池的几种写法,WaitCallback本质上是一个参数为Object类型无返回值的委托

1  public delegate void WaitCallback(object state);

所以符合要求的类型都可以如上述示例代码作为参数进行传递。

 

 // more operations...

执行上下文

每个线程都关联了一个执行上下文数据结构,执行上下文(execution context)包括:

1.安全设置(压缩栈、Thread的Principal属性、winodws身份)。

2.宿主设置(System.Threading.HostExecutionContextManager)。

3.逻辑调用上下文数据(System.Runtime.Remoting.Messaging.CallContext的LogicalGetData和LogicalSetData方法)。

线程执行它的代码时,一些操作会受到线程执行上下文限制,尤其是安全设置的影响。

当主线程使用辅助线程执行任务时,前者的执行上下文“流向”(复制到)辅助线程,这确保了辅助线程执行的任何操作使用的是相同的安全设置和宿主设置。

默认情况下,CLR自动造成初始化线程的执行上下文“流向”任何辅助线程。但这会对性能造成影响。执行上下包含的大量信息采集并复制到辅助线程要耗费时间,如果辅助线程又采用了更多的辅助线程还必须创建和初始化更多的执行上下文数据结构。

System.Threading命名空间的ExecutionContext类,它允许控制线程执行上下文的流动:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //将一些数据放到主函数线程的逻辑调用上下文中
 6             CallContext.LogicalSetData("Action", "Jonins");
 7             //初始化要由另一个线程做的一些事情,线程池线程能访问逻辑上下文数据
 8             ThreadPool.QueueUserWorkItem(state => Console.WriteLine("辅助线程A:"   Thread.CurrentThread.ManagedThreadId   ";Action={0}", CallContext.LogicalGetData("Action")));
 9             //现在阻止主线程执行上下文流动
10             ExecutionContext.SuppressFlow();
11             //初始化要由另一个线程做的一些事情,线程池线程能访问逻辑上下文数据
12             ThreadPool.QueueUserWorkItem(state => Console.WriteLine("辅助线程B:"   Thread.CurrentThread.ManagedThreadId   ";Action={0}", CallContext.LogicalGetData("Action")));
13             //恢复主线程的执行上下文流动,以避免使用更多的线程池线程
14             ExecutionContext.RestoreFlow();
15             Console.ReadKey();
16         }
17     }

结果如下:

图片 12

ExecutionContext类阻止上下文流动以提升程序的性能,对于服务器应用程序,性能的提升可能非常显著。但是客户端应用程序的性能提升不了多少。另外,由于SuppressFlow方法用[SecurityCritical]特性标记,所以某些客户端如Silverlight中是无法调用的。

注意:

1.辅助线程在不需要或者不访问上下文信息时,应阻止执行上下文的流动。

2.执行上下文流动的相关知识,在使用Task对象以及发起异步I/O操作时,同样有用。

 

 }

参考文献

CLR via C#(第4版) Jeffrey Richter

C#高级编程(第10版) C# 6 & .NET Core 1.0   Christian Nagel  

果壳中的C# C#5.0权威指南  Joseph Albahari

         

...

 当我们希望进行一个异步的IO-Bound Operation时,CLR会(通过Windows API)发出一个IRP(I/O Request Packet)。当设备准备妥当,就会找出一个它“最想处理”的IRP(例如一个读取离当前磁头最近的数据的请求)并进行处理,处理完毕后设备将会(通过Windows)交还一个表示工作完成的IRP。CLR会为每个进程创建一个IOCP(I/O Completion Port)并和Windows操作系统一起维护。IOCP中一旦被放入表示完成的IRP之后(通过内部的ThreadPool.BindHandle完成),CLR就会尽快分配一个可用的线程用于继续接下去的任务。

I/O线程

IO线程是.NET专为访问外部资源所引入的一种线程,访问外部资源时为了防止主线程长期处于阻塞状态,.NET为多个I/O操作建立了异步方法。例如:

FileStream:BeginRead、 style="color: #0000ff;">BeginWrite。调用BeginRead/BeginWrite时会发起一个异步操作,但是只有在创建FileStream时传入FileOptions.Asynchronous参数才能获取真正的IOCP支持,否则BeginXXX方法将会使用默认定义在Stream基类上的实现。Stream基类中BeginXXX方法会使用委托的BeginInvoke方法来发起异步调用——这会使用一个额外的线程来执行任务(并不受IOCP支持,可能额外增加性能损耗)。

DNS: style="color: #0000ff;">BeginGetHostByName、 style="color: #0000ff;">BeginResolve。

Socket:BeginAccept、 style="color: #0000ff;">BeginConnect、 style="color: #0000ff;">BeginReceive等等。

WebRequest: style="color: #0000ff;">BeginGetRequestStream、 style="color: #0000ff;">BeginGetResponse。

SqlCommand: style="color: #0000ff;">BeginExecuteReader、 style="color: #0000ff;">BeginExecuteNonQuery等等。这可能是开发一个Web应用时最常用的异步操作了。如果需要在执行数据库操作时得到IOCP支持,那么需要在连接字符串中标记Asynchronous Processing为true(默认为false),否则在调用BeginXXX操作时就会抛出异常。

WebServcie:例如.NET 2.0或WCF生成的Web Service Proxy中的BeginXXX方法、WCF中ClientBase<TChannel>的InvokeAsync方法。

这些异步方法的使用方式都比较类似,都是以Beginxxx开始(内部实现为ThreadPool.BindHandle),以Endxxx结束。

注意

1.对于APM而言必须使用Endxxx结束异步,否则可能会造成资源泄露。

2.委托的BeginInvoke方法并不能获得IOCP支持。

3.IOCP不占用线程。

下面是使用WebRequest的一个示例调用异步API占用I/O线程:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             int workerThreadsCount, completionPortThreadsCount;
 6             ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
 7             Console.WriteLine("剩余工作线程数:{0},剩余IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
 8             //调用WebRequest类的异步API占用IO线程
 9             {
10                 WebRequest webRequest = HttpWebRequest.Create("http://www.cnblogs.com/jonins");
11                 webRequest.BeginGetResponse(result =>
12                 {
13                     Thread.Sleep(2000);
14                     Console.WriteLine(Thread.CurrentThread.ManagedThreadId   ":执行最终响应的回调");
15                     WebResponse webResponse = webRequest.EndGetResponse(result);
16                 }, null);
17             }
18             Thread.Sleep(1000);
19             ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
20             Console.WriteLine("剩余工作线程数:{0},剩余IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
21             Console.ReadKey();
22         }
23     }

执行结果如下:

图片 13

有关I/O线程的内容点到此为止,感觉更多是I/O操作、文件等方面的知识点跟线程池瓜葛不多,想了解更多戳:这里

 

 在Windows操作系统中,Server 2003及之前版本的API也只提供了进程内部单一的线程池,不过在Vista及Server 2008的API中,除了改进线程池的性能之外,还提供了在同一进程内创建多个线程池的接口。很可惜,.NET直到如今的4.0版本,依旧没有提供构建独立线程池的功能。构造一个优秀的线程池是一件相当困难的事情,幸运的是,如果我们需要这方面的功能,可以借助著名的SmartThreadPool,经过那么多年的考验,相信它已经足够成熟了。如果需要,我们还可以对它做一定修改——毕竟在不同情况下,我们对线程池的要求也不完全相同。

线程池工作原理

CLR初始化时,线程池中是没有线程的。在内部,线程池维护了一个操作请求队列。应用程序执行一个异步操作时,会将一个记录项追加到线程池的队列中。线程池的代码从这个队列中读取记录将这个记录项派发给一个线程池线程。如果线程池没有线程,就创建一个新线程。当线程池线程完成工作后,线程不会被销毁,相反线程会返回线程池,在那里进入空闲状态,等待响应另一个请求,由于线程不销毁自身,所以不再产生额外的性能损耗。

程序向线程池发送多条请求,线程池尝试只用这一个线程来服务所有请求,当请求速度超过线程池线程处理任务速度,就会创建额外线程,所以线程池不必创建大量线程。

如果停止向线程池发送任务,池中大量空闲线程将在一段时间后自己醒来终止自己以释放资源(CLR不同版本对这个事件定义不一)。

 

 WebResponse response = request.EndGetResponse(ar);

1.APM&EAP&TAP

.NET支持三种异步编程模式分别为APM、EAP和TAP:

1.基于事件的异步编程设计模式 (EAP,Event-based Asynchronous Pattern)

EAP的编程模式的代码命名有以下特点: 

1.有一个或多个名为 “[XXX]Async” 的方法。这些方法可能会创建同步版本的镜像,这些同步版本会在当前线程上执行相同的操作。
2.该类还可能有一个 “[XXX]Completed” 事件,监听异步方法的结果。
3.它可能会有一个 “[XXX]AsyncCancel”(或只是 CancelAsync)方法,用于取消正在进行的异步操作。

2.异步编程模型(APM,Asynchronous Programming Model)

APM的编程模式的代码命名有以下特点:

1.使用 IAsyncResult 设计模式的异步操作是通过名为[BeginXXX] 和 [EndXXX] 的两个方法来实现的,这两个方法分别开始和结束异步操作 操作名称。例如,FileStream 类提供 BeginRead 和 EndRead 方法来从文件异步读取字节。

2.在调用 [BeginXXX] 后,应用程序可以继续在调用线程上执行指令,同时异步操作在另一个线程上执行。 每次调用 [BeginXXX] 时,应用程序还应调用 [EndXXX] 来获取操作的结果。

3.基于任务的编程模型(TAP,Task-based Asynchronous Pattern)

基于 System.Threading.Tasks 命名空间的 Task 和 Task<TResult>,用于表示任意异步操作。 TAP之后再讨论。关于三种异步操作详细说明请戳:这里 

 只可惜,IO线程池也仅仅是那“一整个”线程池,CLR线程池的缺点IO线程池也一应俱全。例如,在使用异步IO方式读取了一段文本之后,下一步操作往往是对其进行分析,这就进入了计算密集型操作了。但对于计算密集型操作来说,如果使用整个IO线程池来执行,我们无法有效的控制某项任务的运算能力。因此在有些时候,我们在回调函数内部会把计算任务再次交还给独立的线程池。这么做从理论上看会增大线程调度的开销,不过实际情况还得看具体的评测数据。如果它真的成为影响性能的关键因素之一,我们就可能需要使用Native Code来调用IOCP相关API,将回调任务直接交给独立的线程池去执行了。

工作者线程&I/O线程

线程池允许线程在多个CPU内核上调度任务,使多个线程能并发工作,从而高效率的使用系统资源,提升程序的吞吐性。

CLR线程池分为工作者线程与I/O线程两种:

工作者线程(workerThreads):负责管理CLR内部对象的运作,提供”运算能力“,所以通常用于计算密集(compute-bound)性操作。

I/O线程(completionPortThreads):主要用于与外部系统交换信息(如读取一个文件)和分发IOCP中的回调。

注意:线程池会预先缓存一些工作者线程因为创建新线程的代价比较昂贵。

 

public static class ThreadPool

线程池常用方法

ThreadPool常用的几个方法如下

方法 说明
QueueUserWorkItem 启动线程池里的一个线程(工作者线程)
GetMinThreads 检索线程池在新请求预测中能够按需创建的线程的最小数量。
GetMaxThreads 最多可用线程数,所有大于此数目的请求将保持排队状态,直到线程池线程由空闲。
GetAvailableThreads 剩余空闲线程数。
SetMaxThreads 设置线程池中的最大线程数(请求数超过此值则进入队列)。
SetMinThreads 设置线程池最少需要保留的线程数。

 示例代码:

 1         static void Main(string[] args)
 2         {
 3             //声明变量 (工作者线程计数  Io完成端口计数)
 4             int workerThreadsCount, completionPortThreadsCount;
 5             {
 6                 ThreadPool.GetMinThreads(out workerThreadsCount, out completionPortThreadsCount);
 7                 Console.WriteLine("最小工作线程数:{0},最小IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
 8             }
 9             {
10                 ThreadPool.GetMaxThreads(out workerThreadsCount, out completionPortThreadsCount);
11                 Console.WriteLine("最大工作线程数:{0},最大IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
12             }
13             ThreadPool.QueueUserWorkItem((o) => {
14                 Console.WriteLine("占用1个池化线程");
15             });
16             {
17                 ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
18                 Console.WriteLine("剩余工作线程数:{0},剩余IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
19             }
20             Console.ReadKey();
21         }

 执行的结果:

图片 14

注意:

1.线程有内存开销,所以线程池内的线程过多而没有完全利用是对内存的一种浪费,所以需要对线程池限制最小线程数量。 

2.线程池最大线程数是线程池最多可创建线程数,实际情况是线程池内的线程数是按需创建。

 

 使用独立的线程池来控制运算能力的做法很常见,一个典型的案例便是SEDA架构:整个架构由多个Stage连接而成,每个Stage均由一个队列和一个独立的线程池组成,调节器会根据队列中任务的数量来调节线程池内的线程数量,最终使应用程序获得优异的并发能力。

IO完成端口(IOCP)

IO完成端口(IOCP、I/O completion port):IOCP是一个异步I/O的API(可以看作一个消息队列),提供了处理多个异步I/O请求的线程模型,它可以高效地将I/O事件通知给应用程序。IOCP由CLR内部维护,当异步IO请求完成时,设备驱动就会生成一个I/O请求包(IRP、I/O Request Packet),并排队(先入先出)放入完成端口。之后会由I/O线程提取完成IRP并调用之前的委托。

I/O线程&IOCP&IRP:

当执行I/O操作时(同步I/O操作 and 异步I/O操作),都会调用Windows的API方法将当前的线程从用户态转变成内核态,同时生成并初始化一个I/O请求包,请求包中包含一个文件句柄,一个偏移量和一个Byte[]数组。I/O操作向内核传递请求包,根据这个请求包,windows内核确认这个I/O操作对应的是哪个硬件设备。这些I/O操作会进入设备自己的处理队列中,该队列由这个设备的驱动程序维护。

如果是同步I/O操作,那么在硬件设备操作I/O的时候,发出I/O请求的线程由于”等待“(无人任务处理)被Windows变成睡眠状态,当硬件设备完成操作后,再唤醒这个线程。所以性能不高,如果请求数很多,那么休眠的线程数也很多,浪费大量资源。

如果是异步I/O操作(在.Net中,异步的I/O操作都是以Beginxxx形式开始,内部实现为ThreadPool.BindHandle,需要传入一个委托,该委托会随着IRP一路传递到设备的驱动程序),该方法在Windows把I/O请求包发送到设备的处理队列后就会返回。同时,CLR会分配一个可用的线程用于继续执行接下来的任务,当任务完成后,通过IOCP提醒CLR它工作已经完成,当接收到通知后将该委托再放到CLR线程池队列中由IO线程进行回调。

所以:大多数情况下,开发人员使用工作者线程,I/O线程由CLR调用(开发者并不会直接使用)。

 

 在使用异步IO时,访问IO的线程不会被阻塞,逻辑将会继续下去。操作系统会负责把结果通过某种方法通知我们,一般说来,这种方式是“回调函数”。异步IO在执行过程中是不占用应用程序的线程的,因此我们可以用少量的线程发起大量的IO,所以应用程序的响应能力也可以有所提高。此外,同时发起大量IO操作在某些时候会有额外的性能优势,例如磁盘和网络可以同时工作而不互相冲突,磁盘还可以根据磁头的位置来访问就近的数据,而不是根据请求的顺序进行数据读取,这样可以有效减少磁头的移动距离。

线程池与线程

性能:每开启一个新的线程都要消耗内存空间及资源(默认情况下大约1 MB的内存),同时多线程情况下操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程还对性能不利。而线程池其目的是为了减少开启新线程消耗的资源(使用线程池中的空闲线程,不必再开启新线程,以及统一管理线程(线程池中的线程执行完毕后,回归到线程池内,等待新任务))。

时间:无论何时启动一个线程,都需要时间(几百毫秒),用于创建新的局部变量堆,线程池预先创建了一组可回收线程,因此可以缩短过载时间。

线程池缺点:线程池的性能损耗优于线程(通过共享和回收线程的方式实现),但是:

1.线程池不支持线程的取消、完成、失败通知等交互性操作。

2.线程池不支持线程执行的先后次序排序。

3.不能设置池化线程(线程池内的线程)的Name,会增加代码调试难度。

4.池化线程通常都是后台线程,优先级为ThreadPriority.Normal。

5.池化线程阻塞会影响性能(阻塞会使CLR错误地认为它占用了大量CPU。CLR能够检测或补偿(往池中注入更多线程),但是这可能使线程池受到后续超负荷的印象。Task解决了这个问题)。

6.线程池使用的是全局队列,全局队列中的线程依旧会存在竞争共享资源的情况,从而影响性能(Task解决了这个问题方案是使用本地队列)。

 

 上次我们讨论到,在一个.NET应用程序中会有一个CLR线程池,可以使用ThreadPool类中的静态方法来使用这个线程池。我们只要使用QueueUserWorkItem方法向线程池中添加任务,线程池就会负责在合适的时候执行它们。我们还讨论了CLR线程池的一些高级特性,例如对线程的最大和最小数量作限制,对线程创建时间作限制以避免突发的大量任务消耗太多资源等等。

结语

程序员使用线程池更多的是使用线程池内的工作者线程进行逻辑编码。

相对于单独操作线程(Thread)线程池(ThreadPool)能够保证计算密集作业的临时过载不会引起CPU超负荷(激活的线程数量多于CPU内核数量,系统必须按时间片执行线程调度)。

超负荷会影响性能,因为划分时间片需要大量的上下文切换开销,并且使CPU缓存失效,而这些是处理器实现高效的必要调度。

CLR能够将任务进行排序,并且控制任务启动数量,从而避免线程池超负荷。CLR首先运行与硬件内核数量一样多的并发任务,然后通过爬山算法调整并发数量,保证程序切合最优性能曲线。

 

 {

 值得一提的是,对于Windows操作系统来说,它的调度单元是线程,这和线程究竟属于哪个进程并没有关系。举个例子,如果系统中只有两个进程,进程A有5个线程,而进程B有10个线程。在排除其他因素的情况下,进程B占有运算单元的时间便是进程A的两倍。当然,实际情况自然不会那么简单。例如不同进程会有不同的优先级,线程相对于自己所属的进程还会有个优先级;如果一个线程在许久没有执行的时候,或者这个线程刚从“锁”的等待中恢复,操作系统还会对这个线程的优先级作临时的提升——这一切都是牵涉到程序的运行状态,性能等情况的因素,有机会我们在做展开。

独立线程池

 {

 那么.NET提供的线程池又有什么缺点呢?有些朋友说,一个重要的缺点就是功能太简单,例如只有一个队列,没法做到对多个队列作轮询,无法取消任务,无法设定任务优先级,无法限制任务执行速度等等。不过其实这些简单的功能,倒都可以通过在CLR线程池上增加一层(或者说,通过封装CLR线程池)来实现。例如,您可以让放入CLR线程池中的任务,在执行时从几个自定义任务队列中挑选一个运行,这样便达到了对多个队列作轮询的效果。因此,在我看来,CLR线程池的主要缺点并不在此。

 WebRequest request = HttpWebRequest.Create("");

 IO线程池便是为异步IO服务的线程池。

 IO线程池

 我认为,CLR线程池的主要问题在于“大一统”,也就是说,整个进程内部几乎所有的任务都会依赖这个线程池。如前篇文章所说的那样,如Timer和WaitForSingleObject,还有委托的异步调用,.NET框架中的许多功能都依赖这个线程池。这个做法是合适的,但是由于开发人员对于统一的线程池无法做到精确控制,因此在一些特别的需要就无法满足了。举个最常见例子:控制运算能力。什么是运算能力?那么还是从线程讲起吧1。

 WebRequest request = (WebRequest)ar.AsyncState;

 Windows操作系统中有多种异步IO方式,但是性能最高,伸缩性最好的方式莫过于传说中的“IO完成端口(I/O Completion Port,IOCP)”了,这也是.NET中封装的唯一异步IO方式。大约一年半前,老赵写过一篇文章《正确使用异步操作》,其中除了描述计算密集型和IO密集型操作的区别和效果之外,还简单地讲述了IOCP与CLR交互的方式,摘录如下:

 }

 访问IO最简单的方式(如读取一个文件)便是阻塞的,代码会等待IO操作成功(或失败)之后才继续执行下去,一切都是顺序的。但是,阻塞式IO有很多缺点,例如让UI停止响应,造成上下文切换,CPU中的缓存也可能被清除甚至内存被交换到磁盘中去,这些都是明显影响性能的做法。此外,每个IO都占用一个线程,容易导致系统中线程数量很多,最终限制了应用程序的伸缩性。因此,我们会使用“异步IO”这种做法。

  NativeOverlapped包含了一个IOCompletionCallback回调函数及一个缓冲对象,可以通过Overlapped对象创建。Overlapped会包含一个被固定的空间,这里“固定”的含义表示不会因为GC而导致地址改变,甚至不会被置换到硬盘上的Swap空间去。这么做的目的是迎合IOCP的要求,但是很明显它也会降低程序性能。因此,我们在实际编程中几乎不会使用这个方法3。

 我们在一个程序中创建一个线程,安排给它一个任务,便交由操作系统来调度执行。操作系统会管理系统中所有的线程,并且使用一定的方式进行调度。什么是“调度”?调度便是控制线程的状态:执行,等待等等。我们都知道,从理论上来说有多少个处理单元(如2 * 2 CPU的机器便有4个处理单元),就表示操作系统可以同时做几件事情。但是线程的数量会远远超过处理单元的数量,因此操作系统为了保证每个线程都被执行,就必须等一个线程在某个处理器上执行到某个情况的时候,“换”一个新的线程来执行,这便是所谓的“上下文切换(context switch)”。至于造成上下文切换的原因也有多种,可能是某个线程的逻辑决定的,如遇上锁,或主动进入休眠状态(调用Thread.Sleep方法),但更有可能是操作系统发现这个线程“超时”了。在操作系统中会定义一个“时间片(timeslice)”2,当发现一个线程执行时间超过这个时间,便会把它撤下,换上另外一个。这样看起来,多个线程——也就是多个任务在同时运行了。

 static void HandleAsyncCallback(IAsyncResult ar)

 public static bool UnsafeQueueNativeOverlapped(NativeOverlapped* overlapped);

 BeginGetResponse将发起一个利用IOCP的异步IO操作,并在结束时调用HandleAsyncCallback回调函数。那么,这个回调函数是由哪里的线程执行的呢?没错,就是传说中“IO线程池”的线程。.NET在一个进程中准备了两个线程池,除了上篇文章中所提到的CLR线程池之外,它还为异步IO操作的回调准备了一个IO线程池。IO线程池的特性与CLR线程池类似,也会动态地创建和销毁线程,并且也拥有最大值和最小值(可以参考上一篇文章列举出的API)。

 {

 不过事实上,使用Windows API编写IOCP非常复杂。而在.NET中,由于需要迎合标准的APM(异步编程模型),在使用方便的同时也放弃一定的控制能力。因此,在一些真正需要高吞吐量的时候(如编写服务器),不少开发人员还是会选择直接使用Native Code编写相关代码。不过在绝大部分的情况下,.NET中利用IOCP的异步IO操作已经足以获得非常优秀的性能了。使用APM方式在.NET中使用异步IO非常简单,如下:

 }

 request.BeginGetResponse(HandleAsyncCallback, request);

本文由分分快三计划发布,转载请注明来源

关键词: 分分快三计划 C#技术