第3章:使用线程池
在本章中,我们将描述多线程中使用共享资源的常用技术。
3.1 简介:
在之前的章节中我们讨论了创建线程和线程协作的几种方式。现在考虑另一种情况,即只花费极少的时间来完成创建很多异步操作。正如在第一章的简介小节中讨论过的一样,创建线程是昂贵的操作,所以为每个短暂的异步操作创建线程会产生显著的开销。
为了解决该问题,有一个常用的方式叫做池(pooling)。线程池可以成功地适应于任何需要大量短暂的开销大的资源的情形。我们事先分配一定的资源,将这些资源放入到资源池。每次需要新的资源,只需从池中获取一个,而不用创建一个新的。当该资源不再被使用时,就将其返回到池中。(事先分配,用时随取)
.NET线程池是该概念的一种实现。通过System.Threading.ThreadPool类型可以使用线程池。线程池是受.Net通用语言运行时(Common Language Runtime, CLR)管理的。这意味着每个CLR都有一个线程池实例。ThreadPool类型拥有一个QueueUserWorkItem静态方法。该静态方法接受一个委托,代表用户自定义的一个异步操作。该方法被调用后,委托会进入到内部队列中。如果池中没有任何线程,将创建一个新的工作线程(worker thread)并将队列中的第一个委托放入到该工作线程中。
如果想线程池中放入新的操作,当之前的所有操作完成后,很可能只需要重用一个线程来执行这些新的操作。然而。如果放置新的操作过快,线程池将创建更多的线程来执行这些操作。创建太多的线程是有限制的,在这种情况下新的操作将在队列中等待直到线程池的工作线程有能力来执行他们。
note:保持线程中的操作都是短暂的是非常重要的。不要在线程池中放入长时间运行的操作,或者阻塞工作线程。这将导致所有工作线程变得繁忙,从而无法服务用户操作。这会导致性能问题和非常难以调试的错误。(短时间线程)
当停止向线程池中放置新的操作时,线程池最终会删除一定时间后过期的不再使用的线程。这将释放所有哪些不再需要的系统资源。(自动回收)
另一个重要事情是在Asp.Net应用程序中使用线程池要相当小心。Asp.NET基础设施使用自己的线程池,如果在线程池中浪费所有的工作线程,web服务器将不能够服务新的请求。在Asp.Net中只推荐使用输入/输出密集型的异步操作,因为其使用了一个不同的方式,叫做I/O线程。(ASP.net可用I/O线程池)
Note:注意线程池中的工作线程都是后台线程。
3.2 在线程池中调用委托:
本节将展示在线程池中如何异步的执行委托。另外,我们将讨论一个叫做异步编程模式(APM)的方式,这是.Net历史上第一个异步编程模式。
public delegate string RunOnThreadPool(out int threadId); public class Class1 { public void Callback(IAsyncResult ar) { Console.WriteLine("开始回调..."); Console.WriteLine("给回调函数传递一个参数:{0}", ar.AsyncState); Console.WriteLine("该线程是否属于托管线程池:{0}", Thread.CurrentThread.IsThreadPoolThread); Console.WriteLine("当前托管线程的唯一标识符:{0}", Thread.CurrentThread.ManagedThreadId); } public string Test(out int threadId) { Console.WriteLine("开始...."); Console.WriteLine("该线程是否属于托管线程池:{0}", Thread.CurrentThread.IsThreadPoolThread); Thread.Sleep(2000); threadId = Thread.CurrentThread.ManagedThreadId; return string.Format("当前托管线程的唯一标识符:{0}", threadId); } }
static void Main(string[] args) { int threadId = 0; var c1 = new Class1(); var t = new Thread(() => c1.Test(out threadId)); t.Start(); t.Join(); Console.WriteLine("线程ID:{0}", threadId); RunOnThreadPool poolDelegate = c1.Test; var r = poolDelegate.BeginInvoke(out threadId, c1.Callback, "一个异步委托"); r.AsyncWaitHandle.WaitOne(); string result = poolDelegate.EndInvoke(out threadId, r); Console.WriteLine("线程池唯一标识符:{0}", threadId); Console.WriteLine(result); Thread.Sleep(2000); Console.Read(); }
工作原理:当程序运行时,使用就的方式创建了一个线程,然后启动它并等待完成。我们通过打印Thread.CurrentThread.isThreadPoolThread属性值来确保该线程不是来自线程池。我们也打印出了受管理的线程ID来识别代码是被哪个线程执行的。
然后定义了一个委托并调用BeginInvoke方法来运行该委托。BeginInvork方法接受一个回调函数。该回调函数会在异步操作完成后被调用,并且一个用户自定义的状态会传给该回调函数。该状态通常用于区分异步调用。结果,我们得到了一个实现了IAsyncResult接口的result对象。BeginInvorke立即返回了结果,当线程池中工作线程在执行异步操作时,仍允许我们继续其他工作。当需要异步操作的结果时,可以使用beiginInvoke方法调用返回的result对象。我们可以使用result对象的IsCompleted属性轮询结果。但是在本例中,使用AsyncWaitHandle属性来等待直到操作完成。当操作完成后,会得到一个结果,可以通过委托调用EndInvoke方法,将IAsyncResult对象传递给委托参数。
事实上使用AsyncWaitHandle并不是必要的,如果注释掉r.AsyncWaitHandle.WaitOne,代码照样可以成功运行,因为EndInvoke方法事实上会等待异步操作完成。调用EndInvoke方法(或针对其他异步API的EndOperationName方法)是非常重要的,因为该方法会将任何未处理的异常抛出到调用线程中。当使用这种异步API时,请确保始终调用Begin和End方法。(线程池的线程是IsBackground=true)
3.3 向线程池中放入异步操作 ThreadPool.QueueUerWorkItem
private static void AsyncOperation(object state) { Console.WriteLine($"Operation state:{state ?? "(null)"}"); Console.WriteLine($"worker thread id:{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(TimeSpan.FromSeconds(2)); } static void Main(string[] args) { const int x = 1; const int y = 2; const string lambdaState = "lambda state 2"; ThreadPool.QueueUserWorkItem(AsyncOperation); Thread.Sleep(TimeSpan.FromSeconds(1)); ThreadPool.QueueUserWorkItem(AsyncOperation, "async state"); Thread.Sleep(TimeSpan.FromSeconds(1)); ThreadPool.QueueUserWorkItem(state => { Console.WriteLine($"Operation state:{state}"); Console.WriteLine($"Wrker thread id:{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(TimeSpan.FromSeconds(2)); }, "lambda state"); ThreadPool.QueueUserWorkItem(_ => { Console.WriteLine($"Operation state:{x+y},{lambdaState}"); Console.WriteLine($"Wrker thread id:{Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(TimeSpan.FromSeconds(2)); }, "lambda state"); Console.Read(); }
3.4 线程池与并行度:
了解线程池如何工作于大量的异步操作,以及它与创建大量线程的方式的不同之处。
static void UseThreads(int numberOfOperations) { using (var countdown = new CountdownEvent(numberOfOperations)) { Console.WriteLine("Scheduling work by creating threads"); for (int i = 0; i < numberOfOperations; i++) { var thread = new Thread(() => { Console.Write($"{Thread.CurrentThread.ManagedThreadId},"); Thread.Sleep(TimeSpan.FromSeconds(0.1)); countdown.Signal(); }); thread.Start(); } countdown.Wait(); Console.WriteLine(); } } static void UseThreadPool(int numberOfOperations) { using (var countdown = new CountdownEvent(numberOfOperations)) { Console.WriteLine("Starting work on a threadPool"); for (int i = 0; i < numberOfOperations; i++) { ThreadPool.QueueUserWorkItem(_ => { Console.Write($"{Thread.CurrentThread.ManagedThreadId},"); Thread.Sleep(TimeSpan.FromSeconds(0.1)); countdown.Signal(); }); } countdown.Wait(); Console.WriteLine(); } } static void Main(string[] args) { const int numberOfOperations = 500; var sw = new Stopwatch(); sw.Start(); UseThreads(numberOfOperations); sw.Stop(); Console.WriteLine($"Execution time using threads:{sw.ElapsedMilliseconds}"); sw.Restart(); UseThreadPool(numberOfOperations); sw.Stop(); Console.WriteLine($"Execution time using threads:{sw.ElapsedMilliseconds}"); Console.Read(); }
发现用线程创建了500个线程,消耗了大量的内存和cpu,而线程池就使用了是50多个线程,内存和线程数更少了,但是也更耗时了。
3.5 线程池取消
static void AsyncOperation1(CancellationToken token) { Console.WriteLine("Starting the first task"); for (int i = 0; i < 5; i++) { if (token.IsCancellationRequested) { Console.WriteLine("The first task has been canceled."); return; } Thread.Sleep(TimeSpan.FromSeconds(1)); } Console.WriteLine("The first task has completed successfully"); } static void AsyncOperation2(CancellationToken token) { try { Console.WriteLine("Starting the second task"); for (int i = 0; i < 5; i++) { token.ThrowIfCancellationRequested(); Thread.Sleep(TimeSpan.FromSeconds(1)); } Console.WriteLine("The second task has completed successfully"); } catch (OperationCanceledException) { Console.WriteLine("The second task has been canceled."); } } private static void AsyncOperation3(CancellationToken token) { bool cancellationFlag = false; token.Register(() => cancellationFlag = true); Console.WriteLine("Starting the third task"); for (int i = 0; i < 5; i++) { if (cancellationFlag) { Console.WriteLine("The third task has been canceled."); return; } Thread.Sleep(TimeSpan.FromSeconds(1)); } Console.WriteLine("The third task has completed successfully"); } static void Main(string[] args) { using (var cts = new CancellationTokenSource()) { CancellationToken token = cts.Token; ThreadPool.QueueUserWorkItem(_ => AsyncOperation1(token)); Thread.Sleep(TimeSpan.FromSeconds(2)); cts.Cancel(); } using (var cts = new CancellationTokenSource()) { CancellationToken token = cts.Token; ThreadPool.QueueUserWorkItem(_ => AsyncOperation2(token)); Thread.Sleep(TimeSpan.FromSeconds(2)); cts.Cancel(); } using (var cts = new CancellationTokenSource()) { CancellationToken token = cts.Token; ThreadPool.QueueUserWorkItem(_ => AsyncOperation3(token)); Thread.Sleep(TimeSpan.FromSeconds(2)); cts.Cancel(); } Console.Read(); }
本节介绍了CancellationToken、CancellationTokenSource两个新类,采用了三种取消线程的方式
方式1:采用轮询检查CancellationToken.IsCancellationRequested,如果为True,则取消。
方式2:采用抛出一个ThrowIfCancellationRequested异常。
方式3:注册一个回调函数CancellationToken.Register
3.6 在线程池中使用等待事件处理器及超时
static void RunOperations(TimeSpan workerOperationTimeout) { using (var evt = new ManualResetEvent(false)) { using (var cts = new CancellationTokenSource()) { Console.WriteLine("Registering timeout operations..."); var worker = ThreadPool.RegisterWaitForSingleObject(evt, (state, isTimedOut)=> WorkerOperationWait(cts,isTimedOut), null, workerOperationTimeout, true); Console.WriteLine("Starting long running operation..."); ThreadPool.QueueUserWorkItem(_ => WorkerOperation(cts.Token, evt)); Thread.Sleep(workerOperationTimeout.Add(TimeSpan.FromSeconds(2))); worker.Unregister(evt); } } } static void WorkerOperation(CancellationToken token, ManualResetEvent evt) { for (int i = 0; i < 6; i++) { if (token.IsCancellationRequested) { return; } Thread.Sleep(TimeSpan.FromSeconds(1)); } evt.Set(); } static void WorkerOperationWait(CancellationTokenSource cts, bool isTimeout) { if (isTimeout) { cts.Cancel(); Console.WriteLine("Worker opertation timed out and was canceled."); } else { Console.WriteLine("Worker operation succeded."); } } static void Main(string[] args) { RunOperations(TimeSpan.FromSeconds(5)); RunOperations(TimeSpan.FromSeconds(7)); Console.Read(); }