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

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

=============C#.Net 篇目录==============

 

 

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

  1. 异步编程:线程概述及使用
  2. 异步编程:使用线程池管理线程
  3. 异步编程:同步基元对象
  4. 异步编程:并行任务Task
  5. 异步编程:.NET1.0异步编程模型(APM)
  6. 异步编程:.NET 2.0基于事件的异步编程模式(EAP)
  7. 异步编程:.NET 4.5基于任务的异步编程模式(TAP)
  8. 异步编程:能憋出来的话就写---(总结)

 

开始《异步编程:使用线程池管理线程》

 示例程序:异步编程:使用线程池管理线程.rar

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

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

 

对象池

在系统设计中,我们尝尝会使用到“池”的概念。Eg:数据库连接池,socket连接池,线程池,组件队列。“池”可以节省对象重复创建和初始化所耗费的时间。对那些被系统频繁请求和使用的对象,使用此机制可以提高系统运行性能。

“池”是一种“以空间换时间”的做法,我们在内存中保存一系列整装待命的对象,供人随时差遣。与系统效率相比,这些对象所占用的内存空间太微不足道了。

 

流程图:

 

 

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

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

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

 

关键实现代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public  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静态类,为应用程序提供一个由系统管理的辅助线程池,从而使您可以集中精力于应用程序任务而不是线程管理。

在内部,线程池将自己的线程划分工作者线程(辅助线程)和I/O线程。

1)         应用程序要求线程池执行一个异步的计算限制操作时(这个操作可能发起一个I/O限制的操作,eg:发起文件访问),此时使用的就是工作者线程。

2)         I/O线程用于通知你的代码一个异步I/O限制操作完成完成。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public  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);
     ……
}
  1. 线程池线程数

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

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

更改线程池中的最大线程数时需谨慎。将线程池大小设置得太大,可能会造成更频繁的执行上下文切换及加剧资源的争用情况。

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

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

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

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

3)         静态方法GetAvailableThreads()

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

4)         两个参数

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

  1. 排队工作项

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

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

         示例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private  static  void  Example_RegisterWaitForSingleObject()
{
     AutoResetEvent waitHandle = new  AutoResetEvent( false );
     RegisteredWaitHandle registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(
         waitHandle,
         (Object state, bool  timedOut) =>
         {
             Console.WriteLine( "10秒后执行WaitOrTimerCallback" );
         },
         null , TimeSpan.FromSeconds(10), false
      );    
       // 取消等待操作(即不再执行WaitOrTimerCallback委托)      
     //registeredWaitHandle.Unregister(waitHandle);
}

 

执行上下文

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

  1. 上下文切换中的“上下文”是什么?

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

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

  1. 何时执行“上下文切换”?

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

  1. “上下文切换”造成的性能影响

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

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

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

  1. 监视Windows上下文切换工具

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

 

  1. 执行上下文类详解

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

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

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

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public  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();
}

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

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

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

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

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

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

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

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private  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
      );
}

结果如图:

 

         注意:

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

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

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

2)         取消执行上下文流动除了使用ExecutionContext.SuppressFlow()方式外。还可以通过使用ThreadPool的UnsafeQueueUserWorkItem 和UnsafeRegisterWaitForSingleObject来执行委托方法。

 

何时不使用线程池线程

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

  1. 需要前台线程。(线程池线程“始终”是后台线程)
  2. 需要使线程具有特定的优先级。(线程池线程都是默认优先级,“不建议”进行修改)
  3. 任务会长时间占用线程。由于线程池具有最大线程数限制,因此大量占用线程池线程可能会阻止任务启动。
  4. 需要将线程放入单线程单元(STA)。(所有ThreadPool线程“始终”是多线程单元(MTA)中)
  5. 需要具有与线程关联的稳定标识,或使某一线程专用于某一任务。

 

 

 

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

本节到此结束,感谢大家的观赏。

 

 

 

 

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

 


作者:滴答的雨 
出处:http://www.cnblogs.com/heyuquan/ 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 

欢迎园友讨论下自己的见解,及推荐更好资料。 
本文如对读者有帮助,请帮 下。 
谢谢!!!  (快捷回复 : )     (*^_^*)

分类:  C#.Net 篇
标签:  异步编程

你可能感兴趣的:(线程池)