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

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

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

 

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

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

流程图:网络

 

 

         对于对象池的清理一般设计两种方式:数据结构

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

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

 

关键实现代码:app

 

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

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

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);
    ……
}
ThreadPool
  1. 线程池线程数

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 线程。

  1. 排队工做项

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

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

  1. 在一个内核构造可用时调用一个方法

让一个线程不肯定地等待一个内核对象进入可用状态,这对线程的内存资源来讲是一种浪费。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。

         示例:

private 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频繁执行上下文切换,下降程序性能。对于大多数园友不会满意这样的回答,我和你同样也喜欢“知其然,再知其因此然”。

  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就是本小节要说的“执行上下文”。

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();
}
View Code

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

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

但执行上下文的复制会形成必定的性能影响。由于执行上下文中包含大量信息,而收集全部这些信息,再把它们复制到辅助线程,要耗费很多时间。若是辅助线程又采用了更多地辅助线程,还必须建立和初始化更多的执行上下文数据结构。

因此,为了提高应用程序性能,咱们能够阻止执行上下文的流动。固然这只有在辅助线程不须要或者不访问上下文信息的时候才能进行阻止。

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

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

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

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

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
     );
}
View Code

结果如图:

 

 

 注意:

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。 
3. 公共语言运行库或宿主进程将终止线程。

什么时候不使用线程池线程

如今你们都已经知道线程池为咱们提供了方便的异步API及托管的线程管理。那么是否是任什么时候候都应该使用线程池线程呢?固然不是,咱们仍是须要“因地制宜”的,在如下几种状况下,适合于建立并管理本身的线程而不是使用线程池线程:

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

 

 

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

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

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

 

 

 

 

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

 

 摘自:http://www.cnblogs.com/heyuquan/archive/2012/12/23/threadPool-manager.html

相关文章
相关标签/搜索