多线程-线程池

在多线程方面《核心》显得有些深涩难懂,《C#中的多线程》不太系统,翻译得不通顺,倒是《C#线程池参考手册》在部分写得不错,通顺易懂,另一个不错的参考是MSDN的《如何:使用线程池(C#)》
http://msdn.microsoft.com/zh-cn/library/3dasc8as(VS.80).aspx,当然,这仅指对整体概念的了解,具体细节还是主要参考《核心》和MSDN

 

试想一个Web服务器程序,它可以同时响应多个客户端请求,一般的作法是一个主线程,侦听客户请求,对每一个客户请求发起一个新线程,这样就可以异步处理请求而不会占用主线程,例如下面的代码

static void ServerThread()
{
    while(isRunn)
    {
        if(GetReQuest())
        {
            new Thread(WorkerThread).Start());
        }
    }
}

但频繁的创建和释放线程是一个很大的系统开销,另一种作法是事先创建一组线程,内部使用while(true),循环等待在由主线程控制的工作信号上,例如下面的代码

static void WorkerThread()
{
    while (isRunn)
    {
        if (this.go.WaitOne())
        {
            //do something
        }
    }
}

 

但实际实现要无比这个复杂得多,比如初始数量,同步对象、动态数量控制等等,好了,至此为止你已经在使用线程池了,或者说在自定义一个线程池了。

相对于自定义线程池,Windows提供了一组线程池API或者说一个通用线程池,使得线程的创建、撤消和基本管理变得更新容易,这个系统提供的通用线程池并不完全适合每一种环境,但是它常常可以适合你的需要,并且能够节省大量的程序开发时间。.net中的ThreadPool类对应这组API,ThreadPool是一个静态类,它的成员也不多,应用起来十分方便。注意线程池与ThreadPool的区别,自定义线程池并不一定遵循ThreadPool的规则。

 

线程池概念

线程池是指在多线程应用程序的初始化过程中创建线程的集合,当需要线程时,为新任务重用这些线程,而不是创建新线程的过程。这个过程中的线程数量通常是固定的,这取决于内存量和应用程序的需要。当然,也可以动态增加或减少线程数量。池中的每个线程都分派一个任务,当任务完成时,线程就返回线程池中等待下一次分配或被动态销毁。

线程池通常用于服务器应用程序。每个传入的请求都被分配给线程池中的一个线程,因此可以异步处理请求,而不会占用主线程,也不会延迟后续请求的处理,一旦池中某个线程完成任务,它将返回到等待线程队列中,等待被再次使用,或被动态销毁。这种重用使应用程序可以避免为每个任务创建新线程或保留过多线程的开销。

 

 

ThreadPool的应用环境及限制

适用环境:

线程池适用于需要频繁创建删除线程,而这些线程的生存周期又比较短的情况

使用限制:

  • 线程池不适合又大又长的任务
  • 一个进程只能有一个线程池
  • CLR负责调度线程池中的线程,为线程分配任务,并在任务完成时将线程释放回池中。如果任务已添加到队列中,就不能直接取消任务
  • 如果需要标识线程、执行线程操作,如启动、挂起、中止,设置优先级、Join等,那么线程池不能完成这样的工作
  • 如果分配给线程池中的一个线程的任务被锁定,那么这个线程再不会再释放回池中,当然这种情况可以使用有效的编程技巧来避免
  • 线程池中的线程为后台线程,即它们的 IsBackground 属性为 true。这意味着在所有的前台线程都已退出后,ThreadPool 线程不会让应用程序保持运行。

 

ThreadPool的大小

ThreadPool维护的线程池中有两种线程工作线程WorkerThread和完成端口(异步I/O)线程CompletionPortThread(在Win32 API中,通过不同的dwFlags可以使QueueUserWorkItem或RegisterWaitForSingleObject使用异步I/O线程),这里主要讨论前者

总体来说是这样的:第一次的线程池线程调度方法如QueueUserWorkItem时就会创建线程池,然后创建第一个线程,线程的数量根据需求动态变化,但控制在MinWorkerThreads和MaxWrokerThreads之间,如果所有线程都繁忙且线程数达到MaxWorkerThreads上限时,则额外的任务将放入队列中,直到有线程可用时才能够得到处理。看以下代码:

static object locker = new object();
static int k = 0, l = 0;
static ManualResetEvent mEvent = new ManualResetEvent(false);
static void Main(string[] args)
{
    int maxWkThreads = 0, maxCpThreads = 0;
    int minWkThreads = 0, minCpThreads = 0;
    int avWkThreads = 0, avCpThreads = 0;

    Console.ReadLine();

    //最大线程数
    ThreadPool.GetMaxThreads(out maxWkThreads, out maxCpThreads);
    Console.WriteLine("MaxWorkerThreads:{0}, MaxCompletionPortThreas:{1}", maxWkThreads, maxCpThreads);

    //最小线程数
    ThreadPool.GetMinThreads(out minWkThreads, out minCpThreads);
    Console.WriteLine("MinWorkerThreads:{0}, MinCompletionPortThreas:{1}", minWkThreads, minCpThreads);

    //启动最大工作线程数+3个工作线程
    Console.WriteLine("start {0} worker threads", maxWkThreads + 3);
    for(int i = 0; i < maxWkThreads + 3; i++)
        ThreadPool.QueueUserWorkItem(TProc);

    Console.ReadLine();

    //可用线程数
    ThreadPool.GetAvailableThreads(out avWkThreads, out avCpThreads);
    Console.WriteLine("AvailableWorkerThreads:{0}, AvailableCompletionPortThreads{1}", avWkThreads, avCpThreads);
    if (avWkThreads == 0)
        Console.WriteLine("busy and full now");

    //调整最大工作线程数+3
    Console.WriteLine("set MaxWorkerThreads + 3");
    ThreadPool.SetMaxThreads(maxWkThreads + 3, maxCpThreads);

    Console.ReadLine();

    //收回线程
    mEvent.Set();

    Console.ReadLine();
}

//一个线程回调
static void TProc(object state)
{
    lock (locker)
    {
        Console.WriteLine("thread:{0} start", ++k);
    }

    mEvent.WaitOne();

    lock (locker)
    {
        Console.WriteLine("thread:{0} exit", ++l);
    }
}

Get/Set MaxThreads用于获取和设置线程池中线程数量上限,默认是每CPU 25个工作线程(上述代码在双核CPU上输出是50),可以通过mscoree.h文件中的CorSetMaxThreads成员加以改变,也可以在代码中通过SetMaxThreads随时改变成任意大小(如53)

Get/Set MinThreads用于获取和设置线程池中线程数量下限,即使是在所有线程都处于空闲状态时,线程池也会维持最小的可用线程数,以便队列任务可以立即启动。将终止超过此最小数目的空闲线程,以节省系统资源。默认情况下,每个处理器维持一个空闲线程(上述代码在双核CPU上输出是2)。

GetAvailableThreads用户获取线程池线程的最大数目和当前活动(繁忙)数目之间的差值,我们知道,当所有线程繁忙且线程数已达到上限(这里是50个)时,新任务将被排队,直到线程可用时才能得到处理,通过GetAvailableThreads可以预先知道繁忙线程数是否达到上限

ThreadPool没有方法可以知道当前线程池中的线程数量(空闲或繁忙),唯一的方法是通过资源管理器,上述代码初始时线程数是11,之后增加到61,再之后是64,收回线程几分钟后,线程数量降到了13(11+2,其中2是MinThreads)

 

ThreadPool-线程调度

ThreadPool提供了四种线程调度方法,其中重要的两个是QueueUserWorkItem和RegisterWaitForSingleObject

QueueUserWorkItem

QueueUserWorkItem有传参和不传参两种调用形式

public static bool QueueUserWorkItem (
    WaitCallback callBack
)

public static bool QueueUserWorkItem (
    WaitCallback callBack,
    Object state
)

它根据需要创建新线程,并将回调排入任务队列,然后立即返回,至于说回调什么时候执行则是线程池的事了,在返回值方面,如果将回调用方法成功排入队列,则为 true;否则为 false, 注意,这个返回值只是说明是否排队成功,与回调是被立即执行还是排队等待无关。

 

RegisterWaitForSingleObject

RegisterWaitForSingleObject有四种调用形式,区别只在于等待时间的设置格式。R的工作原理相对Q有些特殊,R的第一次调用会使线程池创建一个专门的等待线程用于等待操作,这个线程不属于工作线程范围,因此以下代码最后的AvailableWorkerThreads仍然是50

 

static ManualResetEvent mEvent = new ManualResetEvent(false);
static void Main(string[] args)
{
    int maxWkThreads = 0, maxCpThreads = 0;
    int avWkThreads = 0, avCpThreads = 0;
    ThreadPool.GetMaxThreads(out maxWkThreads, out maxCpThreads);
    Console.WriteLine("MaxWorkerThreads:{0}, MaxCompletionPortThreas:{1}", maxWkThreads, maxCpThreads);

    Console.WriteLine("Register 3 Wait");
    for (int i = 0; i < 3; i++ )
        ThreadPool.RegisterWaitForSingleObject(mEvent, TProc, null, Timeout.Infinite, false);
    
    ThreadPool.GetAvailableThreads(out avWkThreads, out avCpThreads);
    Console.WriteLine("AvailableWorkerThreads:{0}, AvailableCompletionPortThreads{1}", avWkThreads, avCpThreads);

    Console.ReadLine();
    
}

static void TProc(object state, bool timeOut)
{
}

 

这个专门的等待线程在内部使用WaitAny和等待计时器进行等待,按照《核心》的说法,由于WaitForMultipleObjects一次只能同时等待64个对象,算上等待计时器,因此,每隔63个对象后,就会增加一个等待线程。

当所等待的对象有信号或达到超时时间时,专门的等待线程就会将回调用排入任务队列中

RegisterWaitForSingleObject返回一个RegisteredWaitHandle,若要取消等待操作,请调用 RegisteredWaitHandle.Unregister 方法

要注意,因为等待线程使用 Win32 WaitForMultipleObjects 函数来监视已注册的等待操作。因此,如果必须在对 RegisterWaitForSingleObject 的多个调用中使用相同的本机操作系统句柄,则必须使用 Win32DuplicateHandle 函数重复该句柄。请注意,不应为传递到 RegisterWaitForSingleObject 的事件对象发出脉冲,这是因为等待线程可能不会在重置前检测到该事件已发过信号。

自定义线程池编程请参考《C#线程参考手册》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值