C#多线程学习笔记(二)线程池

    在此之前我们曾提到过,创建线程是昂贵的操作,如果为每个短暂的异步操作创建线程会产生显著的开销。为了解决此问题,一个常用的方式叫做“池”。线程池可以成功地适用于任何需要大量但短暂的资源的情形。我们事先分配一定的资源,将这些资源放入到资源池。每次需要新的资源,只要从池中获取一个,而不用创建一个新的。当该资源不再使用时,就将其返还到池中。

    .NET线程池是该概念的一种实现。通过System.Threading.ThreadPool类型可以使用线程池。线程池是受.NET通用语言运行时(CLR,Common Language Runtime,注1)管理的。这意味着每个CLR都有一个线程池实例。ThreadPool类型拥有一个静态的QueueUserWorkItem方法。该方法接受一个委托,代表用户自定义的一个异步操作。在该方法被调用后,委托会进入内部队列中。如果池中没有任何线程,将创建一个新的工作线程(线程池线程),并将队列中第一个委托放入到该工作线程中。

    如果向线程池中放入新的操作,当之前的一些操作完成后,很可能只需要重用一个线程来执行这些新的操作。当然,如果放置新操作的速度过快,线程池就需要创建更多的线程来执行这些操作。创建太多线程是受限制的,这种情况下新的操作将在队列中等待直到线程池中的工作线程有能力来处理它们。因此,保持线程池线程中的操作都是短暂的很重要。不要在线程池中放入长时间运行的操作,或者阻塞线程池线程,这将影响所有工作线程使其变得繁忙,从而导致服务性能问题和一些难以调试的错误。

    线程池使用了较少的线程,但是以比平常更慢的速度来执行异步操作(一些操作会在等待队列中等待)。线程池引擎每隔一段时间会创建出额外的空闲线程,然后通过这些线程从全局队列中挑选出正在等待的工作项(通过静态方法ThreadPool.SetMaxThread能够修改默认的最大工作线程数和完成端口线程数)。当停止向线程池中放置新操作时,线程池最终会删除一定时间后过期的不再使用的线程。

    注1:有关CLR的更多知识,可以去看.NET框架方面的内容。


一    在线程池中执行异步委托,APM模式(异步编程模型)

    BeginOperationName和EndOperationName方法与.NET中的IAsyncResult对象等方式被称为异步编程模型(或APM模式),这样的方法被称为异步方法,该模式也被应用于多个.NET类库的API中,但在现代编程中,更推荐使用任务并行库(Task Parallel Library,简称TPL)来组织异步API,在后续的章节中我们再学习这些内容。

    这里我们使用BeginInvoke方法来运行一个委托,该方法接受一个回调函数。该回调函数会在异步操作完成后被调用,并且一个用户自定义的状态对象会传给该回调函数。该回调函数会在异步操作完成后被调用。最终,我们得到了一个实现了IAsyncResult接口的result对象。

    当线程池的工作线程在执行异步操作时,仍允许我们继续其他工作。当需要异步操作的结果时,可用使用BeginInvoke方法调用返回的IAsyncResult对象。我们可以使用IAsyncResult对象的IsComplete属性轮询结果。但这里我们使用了AsyncWaitHandle属性来等待直到操作完成。(事实上这两种方式都不是必要的,最终我们会调用EndInvoke方法来将IAsyncResult对象传递给委托参数,而EndInvoke方法会等待异步操作完成。)

    在使用这种异步API时调用EndInvoke方法是非常重要,EndInvoke会将任何未处理的异常抛回到调用线程中。

    准备一个回调函数:

    定义一个委托和一个相应的Test操作:

    在Main函数中,我们先使用旧的方式创建一个线程并启动它,等待其完成。随后我们使用BeginInvoke方法来运行RunOnThreadPool委托,(这里使用AsyncWaitHandler.WaitOne方法等待异步操作完成,之前已说过,将这句注释掉是不影响的,因为EndInvoke同样回等待直到异步操作完成),调用EndInvoke方法将IAsyncResult对象传递给委托参数。

下为运行结果:


    如果我们将最后的Thread.Sleep方法注释掉(同时确保直到Main方法结尾都没有其他代码),回调函数将不会再执行。这是因为主线程完成后,所有后台线程会被停止。这与前台后台线程的关系是一致的。

    不得不提的是,这种将状态对象传递给方法回调的机制即冗余又过时,在C#有了lambda函数和闭包之后就不再需要使用它了(注2)。在上面的代码中,我们将一段字符串"a delegate asynchronous call"当作状态对象传给了回调函数,但这些步骤可以简化为下面这段代码:

运行结果如下:

注2:闭包机制允许我们向异步操作传递一个以上的具有静态类型的对象。有关闭包机制和lambda函数的相关内容大家可以多看看多学点,虽然现在OOP大行其道,一个有趣的趋势是越来越多的OOP语言都开始重视函数式设计的优点了。

二    取消,CancellationTokenSource和CancellationToken

    在线程池中取消一个异步操作可以使用CancellationTokenSource和CancellationToken类。它们在.NET4.0被引入,目前是实现异步操作的取消操作的事实标准。

    调用CancellationTokenSource的Cancel方法可以提交一个取消请求,如果给异步操作传入了这个CancellationTokenSource的CancellationToken,我们可以通过下面三种方式来判断是否有取消请求:

    1.轮询检查CancellationToken.IsCancellationRequest属性;

    2.抛出一个OperationCancelledException异常,这使得我们可以在操作之外控制取消过程;

    3.使用CancellationTokem.Register方法来注册一个回调函数,当操作被取消时,线程池将调用该回调函数,这提供了链式传递一个取消策略到另一个异步操作过程种的可能。

这里列出三种方式的测试代码:

Main函数内容:

三    等待及超时

    线程池还有一个有用的方法:ThreadPool.RegisterWaitForSingleObject,该方法允许我们将回调函数放入线程池的队列中。当提供的等待事件收到信号或超时时,该回调函数将被调用。这使得我们可以为线程池的操作实现超时功能。

    首先向线程池中放入一个耗时长的操作,它允许6s后,一旦成功完成,就会设置一个ManualResetEvent信号类。同时我们使用RegisterWaitForSingleObject注册第二个异步操作,当从ManualResetEvent收到一个信号后,该异步操作会被调用。若第一个操作超时,我们就调用CancellationToken的Cancel方法来取消第一个操作。

Main方法中我们分别为操作提供5s和7s的超时时间,并观察操作的执行情况:

    虽然我们强调了尽量不要使线程池线程阻塞并等待,但实际应用中还是可能遇到有大量的线程必须处于阻塞状态并等待一些其他多线程事件发信号的状况。这时候以上方式将非常有用,它使得我们无需阻塞所有这样的线程,可以提前释放这些线程除非信号事件被设置。

四    计时器与周期性操作

    如果观察过System.Threading.Timer的构造函数可以看到,Timer的构造函数允许传入一个到期回调函数,这使得我们可以在线程池中创建周期性的异步操作。


Main函数中我们先设定在1s后开始执行,周期为2s,这样过来6s后我们使用Timer的Change方法设定在1s后开始执行,周期为4s.


运行结果如下:

五    BackgroundWorker

    BackgroundWorker提供了另一种异步编程的方式,借助于该对象,可以将异步代码组织为一系列的事件与事件订阅者。BackgroundWorker的RunWorkerAsync方法可以运行所有订阅了其DoWork事件的操作,同时BackgroundWorker提供了一个CancelAsync方法来取消操作。其ReportProgress事件可以在途中触发其ProgressChanged事件。运行完毕时BackgroundWorker将触发RunWorkerCompleted事件。

    通过DoWorkEventArg的相关属性可以获得或设置操作的参数和结果,及是否要被取消。

    通过ProgressChangedEventArg的ProgressPercentage属性可以或者操作进行的百分比(通常是你在操作中自己设置的)。

    通过RunWorkerCompletedEventArg的相关属性可以或知操作是成功完成还是被取消或出错了。

猜你喜欢

转载自blog.csdn.net/saasanken/article/details/79512412