目录git
在本章中,主要介绍线程池(ThreadPool)的使用;在C#中它叫System.Threading.ThreadPool
,在使用线程池以前首先咱们得明白一个问题,那就是为何要使用线程池。其主要缘由是建立一个线程的代价是昂贵的,建立一个线程会消耗不少的系统资源。编程
那么线程池是如何解决这个问题的呢?线程池在初始时会自动建立必定量的线程供程序调用,使用时,开发人员并不直接分配线程,而是将须要作的工做放入线程池工做队列中,由线程池分配已有的线程进行处理,等处理完毕后线程不是被销毁,而是从新回到线程池中,这样节省了建立线程的开销。c#
可是在使用线程池时,须要注意如下几点,这将很是重要。设计模式
- 线程池不适合处理长时间运行的做业,或者处理须要与其它线程同步的做业。
- 避免将线程池中的工做线程分配给I/O首先的任务,这种任务应该使用TPL模型。
- 如非必须,不要手动设置线程池的最小线程数和最大线程数,CLR会自动的进行线程池的扩张和收缩,手动干预每每让性能更差。
本节展现的是如何在线程池中如何异步的执行委托,而后将介绍一个叫异步编程模型(Asynchronous Programming Model,简称APM)的异步编程方式。安全
在本节及之后,为了下降代码量,在引用程序集声明位置默认添加了using static System.Console
和using static System.Threading.Thead
声明,这样声明可让咱们在程序中少些一些意义不大的调用语句。多线程
演示代码以下所示,使用了普通建立线程和APM方式来执行同一个任务。闭包
static void Main(string[] args) { int threadId = 0; RunOnThreadPool poolDelegate = Test; var t = new Thread(() => Test(out threadId)); t.Start(); t.Join(); WriteLine($"手动建立线程 Id: {threadId}"); // 使用APM方式 进行异步调用 异步调用会使用线程池中的线程 IAsyncResult r = poolDelegate.BeginInvoke(out threadId, Callback, "委托异步调用"); r.AsyncWaitHandle.WaitOne(); // 获取异步调用结果 string result = poolDelegate.EndInvoke(out threadId, r); WriteLine($"Thread - 线程池工做线程Id: {threadId}"); WriteLine(result); Console.ReadLine(); } // 建立带一个参数的委托类型 private delegate string RunOnThreadPool(out int threadId); private static void Callback(IAsyncResult ar) { WriteLine("Callback - 开始运行Callback..."); WriteLine($"Callback - 回调传递状态: {ar.AsyncState}"); WriteLine($"Callback - 是否为线程池线程: {CurrentThread.IsThreadPoolThread}"); WriteLine($"Callback - 线程池工做线程Id: {CurrentThread.ManagedThreadId}"); } private static string Test(out int threadId) { string isThreadPoolThread = CurrentThread.IsThreadPoolThread ? "ThreadPool - ": "Thread - "; WriteLine($"{isThreadPoolThread}开始运行..."); WriteLine($"{isThreadPoolThread}是否为线程池线程: {CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(2)); threadId = CurrentThread.ManagedThreadId; return $"{isThreadPoolThread}线程池工做线程Id: {threadId}"; }
运行结果以下图所示,其中以Thread开头的为手动建立的线程输出的信息,而TheadPool为开始线程池任务输出的信息,Callback为APM模式运行任务结束后,执行的回调方法,能够清晰的看到,Callback的线程也是线程池的工做线程。异步
在上文中,使用BeginOperationName/EndOperationName
方法和.Net中的IAsyncResult
对象的方式被称为异步编程模型(或APM模式),这样的方法被称为异步方法。使用委托的BeginInvoke
方法来运行该委托,BeginInvoke
接收一个回调函数,该回调函数会在任务处理完成后背调用,而且能够传递一个用户自定义的状态给回调函数。async
如今这种APM编程方式用的愈来愈少了,更推荐使用任务并行库(Task Parallel Library,简称TPL)来组织异步API。异步编程
本节将介绍如何将异步操做放入线程池中执行,而且如何传递参数给线程池中的线程。本节中主要用到的是ThreadPool.QueueUserWorkItem()
方法,该方法可将须要运行的任务经过委托的形式传递给线程池中的线程,而且容许传递参数。
使用比较简单,演示代码以下所示。演示了线程池使用中如何传递方法和参数,最后须要注意的是使用了Lambda
表达式和它的闭包机制。
static void Main(string[] args) { const int x = 1; const int y = 2; const string lambdaState = "lambda state 2"; // 直接将方法传递给线程池 ThreadPool.QueueUserWorkItem(AsyncOperation); Sleep(TimeSpan.FromSeconds(1)); // 直接将方法传递给线程池 而且 经过state传递参数 ThreadPool.QueueUserWorkItem(AsyncOperation, "async state"); Sleep(TimeSpan.FromSeconds(1)); // 使用Lambda表达式将任务传递给线程池 而且经过 state传递参数 ThreadPool.QueueUserWorkItem(state => { WriteLine($"Operation state: {state}"); WriteLine($"工做线程 id: {CurrentThread.ManagedThreadId}"); Sleep(TimeSpan.FromSeconds(2)); }, "lambda state"); // 使用Lambda表达式将任务传递给线程池 经过 **闭包** 机制传递参数 ThreadPool.QueueUserWorkItem(_ => { WriteLine($"Operation state: {x + y}, {lambdaState}"); WriteLine($"工做线程 id: {CurrentThread.ManagedThreadId}"); Sleep(TimeSpan.FromSeconds(2)); }, "lambda state"); ReadLine(); } private static void AsyncOperation(object state) { WriteLine($"Operation state: {state ?? "(null)"}"); WriteLine($"工做线程 id: {CurrentThread.ManagedThreadId}"); Sleep(TimeSpan.FromSeconds(2)); }
运行结果以下图所示。
在本节中,主要是使用普通建立线程和使用线程池内的线程在任务量比较大的状况下有什么区别,咱们模拟了一个场景,建立了不少不一样的线程,而后分别使用普通建立线程方式和线程池方式看看有什么不一样。
static void Main(string[] args) { const int numberOfOperations = 500; var sw = new Stopwatch(); sw.Start(); UseThreads(numberOfOperations); sw.Stop(); WriteLine($"使用线程执行总用时: {sw.ElapsedMilliseconds}"); sw.Reset(); sw.Start(); UseThreadPool(numberOfOperations); sw.Stop(); WriteLine($"使用线程池执行总用时: {sw.ElapsedMilliseconds}"); Console.ReadLine(); } static void UseThreads(int numberOfOperations) { using (var countdown = new CountdownEvent(numberOfOperations)) { WriteLine("经过建立线程调度工做"); for (int i = 0; i < numberOfOperations; i++) { var thread = new Thread(() => { Write($"{CurrentThread.ManagedThreadId},"); Sleep(TimeSpan.FromSeconds(0.1)); countdown.Signal(); }); thread.Start(); } countdown.Wait(); WriteLine(); } } static void UseThreadPool(int numberOfOperations) { using (var countdown = new CountdownEvent(numberOfOperations)) { WriteLine("使用线程池开始工做"); for (int i = 0; i < numberOfOperations; i++) { ThreadPool.QueueUserWorkItem(_ => { Write($"{CurrentThread.ManagedThreadId},"); Sleep(TimeSpan.FromSeconds(0.1)); countdown.Signal(); }); } countdown.Wait(); WriteLine(); } }
执行结果以下,可见使用原始的建立线程执行,速度很是快。只花了2秒钟,可是建立了500多个线程,而使用线程池相对来讲比较慢,花了9秒钟,可是只建立了不多的线程,为操做系统节省了线程和内存空间,但花了更多的时间。
在以前的文章中有提到,若是须要终止一个线程的执行,那么可使用Abort()
方法,可是有诸多的缘由并不推荐使用Abort()
方法。
这里推荐的方式是使用协做式取消(cooperative cancellation),这是一种可靠的技术来安全取消再也不须要的任务。其主要用到CancellationTokenSource
和CancellationToken
两个类,具体用法见下面演示代码。
如下延时代码主要是实现了使用CancellationToken
和CancellationTokenSource
来实现任务的取消。可是任务取消后能够进行三种操做,分别是:直接返回、抛出ThrowIfCancellationRequesed
异常和执行回调。详细请看代码。
static void Main(string[] args) { // 使用CancellationToken来取消任务 取消任务直接返回 using (var cts = new CancellationTokenSource()) { CancellationToken token = cts.Token; ThreadPool.QueueUserWorkItem(_ => AsyncOperation1(token)); Sleep(TimeSpan.FromSeconds(2)); cts.Cancel(); } // 取消任务 抛出 ThrowIfCancellationRequesed 异常 using (var cts = new CancellationTokenSource()) { CancellationToken token = cts.Token; ThreadPool.QueueUserWorkItem(_ => AsyncOperation2(token)); Sleep(TimeSpan.FromSeconds(2)); cts.Cancel(); } // 取消任务 并 执行取消后的回调函数 using (var cts = new CancellationTokenSource()) { CancellationToken token = cts.Token; token.Register(() => { WriteLine("第三个任务被取消,执行回调函数。"); }); ThreadPool.QueueUserWorkItem(_ => AsyncOperation3(token)); Sleep(TimeSpan.FromSeconds(2)); cts.Cancel(); } ReadLine(); } static void AsyncOperation1(CancellationToken token) { WriteLine("启动第一个任务."); for (int i = 0; i < 5; i++) { if (token.IsCancellationRequested) { WriteLine("第一个任务被取消."); return; } Sleep(TimeSpan.FromSeconds(1)); } WriteLine("第一个任务运行完成."); } static void AsyncOperation2(CancellationToken token) { try { WriteLine("启动第二个任务."); for (int i = 0; i < 5; i++) { token.ThrowIfCancellationRequested(); Sleep(TimeSpan.FromSeconds(1)); } WriteLine("第二个任务运行完成."); } catch (OperationCanceledException) { WriteLine("第二个任务被取消."); } } static void AsyncOperation3(CancellationToken token) { WriteLine("启动第三个任务."); for (int i = 0; i < 5; i++) { if (token.IsCancellationRequested) { WriteLine("第三个任务被取消."); return; } Sleep(TimeSpan.FromSeconds(1)); } WriteLine("第三个任务运行完成."); }
运行结果以下所示,符合预期结果。
本节将介绍如何在线程池中使用等待任务和如何进行超时处理,其中主要用到ThreadPool.RegisterWaitForSingleObject()
方法,该方法容许传入一个WaitHandle
对象,和须要执行的任务、超时时间等。经过使用这个方法,可完成线程池状况下对超时任务的处理。
演示代码以下所示,运行了两次使用ThreadPool.RegisterWaitForSingleObject()
编写超时代码的RunOperations()
方法,可是所传入的超时时间不一样,因此形成一个必然超时和一个不会超时的结果。
static void Main(string[] args) { // 设置超时时间为 5s WorkerOperation会延时 6s 确定会超时 RunOperations(TimeSpan.FromSeconds(5)); // 设置超时时间为 7s 不会超时 RunOperations(TimeSpan.FromSeconds(7)); } static void RunOperations(TimeSpan workerOperationTimeout) { using (var evt = new ManualResetEvent(false)) using (var cts = new CancellationTokenSource()) { WriteLine("注册超时操做..."); // 传入同步事件 超时处理函数 和 超时时间 var worker = ThreadPool.RegisterWaitForSingleObject(evt , (state, isTimedOut) => WorkerOperationWait(cts, isTimedOut) , null , workerOperationTimeout , true); WriteLine("启动长时间运行操做..."); ThreadPool.QueueUserWorkItem(_ => WorkerOperation(cts.Token, evt)); Sleep(workerOperationTimeout.Add(TimeSpan.FromSeconds(2))); // 取消注册等待的操做 worker.Unregister(evt); ReadLine(); } } static void WorkerOperation(CancellationToken token, ManualResetEvent evt) { for (int i = 0; i < 6; i++) { if (token.IsCancellationRequested) { return; } Sleep(TimeSpan.FromSeconds(1)); } evt.Set(); } static void WorkerOperationWait(CancellationTokenSource cts, bool isTimedOut) { if (isTimedOut) { cts.Cancel(); WriteLine("工做操做超时并被取消."); } else { WriteLine("工做操做成功."); } }
运行结果以下图所示,与预期结果相符。
计时器是FCL提供的一个类,叫System.Threading.Timer
,可要结果与建立周期性的异步操做。该类使用比较简单。
如下的演示代码使用了定时器,并设置了定时器延时启动时间和周期时间。
static void Main(string[] args) { WriteLine("按下回车键,结束定时器..."); DateTime start = DateTime.Now; // 建立定时器 _timer = new Timer(_ => TimerOperation(start), null , TimeSpan.FromSeconds(1) , TimeSpan.FromSeconds(2)); try { Sleep(TimeSpan.FromSeconds(6)); _timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(4)); ReadLine(); } finally { //实现了IDispose接口 要及时释放 _timer.Dispose(); } } static Timer _timer; static void TimerOperation(DateTime start) { TimeSpan elapsed = DateTime.Now - start; WriteLine($"离 {start} 过去了 {elapsed.Seconds} 秒. " + $"定时器线程池 线程 id: {CurrentThread.ManagedThreadId}"); }
运行结果以下所示,可见定时器根据所设置的周期时间循环的调用TimerOperation()
方法。
本节主要介绍BackgroundWorker
组件的使用,该组件实际上被用于Windows窗体应用程序(Windows Forms Application,简称 WPF)中,经过它实现的代码能够直接与UI控制器交互,更加自认和好用。
演示代码以下所示,使用BackgroundWorker
来实现对数据进行计算,而且让其支持报告工做进度,支持取消任务。
static void Main(string[] args) { var bw = new BackgroundWorker(); // 设置可报告进度更新 bw.WorkerReportsProgress = true; // 设置支持取消操做 bw.WorkerSupportsCancellation = true; // 须要作的工做 bw.DoWork += Worker_DoWork; // 工做处理进度 bw.ProgressChanged += Worker_ProgressChanged; // 工做完成后处理函数 bw.RunWorkerCompleted += Worker_Completed; bw.RunWorkerAsync(); WriteLine("按下 `C` 键 取消工做"); do { if (ReadKey(true).KeyChar == 'C') { bw.CancelAsync(); } } while (bw.IsBusy); } static void Worker_DoWork(object sender, DoWorkEventArgs e) { WriteLine($"DoWork 线程池 线程 id: {CurrentThread.ManagedThreadId}"); var bw = (BackgroundWorker)sender; for (int i = 1; i <= 100; i++) { if (bw.CancellationPending) { e.Cancel = true; return; } if (i % 10 == 0) { bw.ReportProgress(i); } Sleep(TimeSpan.FromSeconds(0.1)); } e.Result = 42; } static void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e) { WriteLine($"已完成{e.ProgressPercentage}%. " + $"处理线程 id: {CurrentThread.ManagedThreadId}"); } static void Worker_Completed(object sender, RunWorkerCompletedEventArgs e) { WriteLine($"完成线程池线程 id: {CurrentThread.ManagedThreadId}"); if (e.Error != null) { WriteLine($"异常 {e.Error.Message} 发生."); } else if (e.Cancelled) { WriteLine($"操做已被取消."); } else { WriteLine($"答案是 : {e.Result}"); } }
运行结果以下所示。
在本节中,使用了C#中的另一个语法,叫事件(event)。固然这里的事件不一样于以前在线程同步章节中提到的事件,这里是观察者设计模式的体现,包括事件源、订阅者和事件处理程序。所以,除了异步APM模式意外,还有基于事件的异步模式(Event-based Asynchronous Pattern,简称 EAP)。
本文主要参考了如下几本书,在此对这些做者表示由衷的感谢大家提供了这么好的资料。
- 《CLR via C#》
- 《C# in Depth Third Edition》
- 《Essential C# 6.0》
- 《Multithreading with C# Cookbook Second Edition》
- 《C#多线程编程实战》
源码下载点击连接 示例源码下载