在C#多线程之线程池篇中,我们将学习多线程访问共享资源的一些通用的技术,我们将学习到以下知识点:
- 在线程池中调用委托
- 在线程池中执行异步操作
- 线程池和并行度
- 实现取消选项
- 使用等待句柄和超时
- 使用计时器
- 使用BackgroundWorker组件
在前面的“C#多线程之基础篇”以及“C#多线程之线程同步篇”中,我们学习了如何创建线程以及如何使用多线程协同工作,在这一篇中,我们将学习另外一种场景,就是我们需要创建许多花费时间非常短的异步操作来完成某些工作。我们知道创建一个线程是非常昂贵的,因此,对于每个花费时间非常短的异步操作都创建一个线程是不合适的。
我们可以使用线程池来解决以上问题,我们可以在线程池中分配一定数量的线程,每当我们需要一个线程时,我们只需要在线程池中取得一个线程即可,而不需要创建一个新的线程,当我们使用完一个线程时,我们仅仅需要把线程重新放入线程池中即可。
我们可以使用System.Threading.ThreadPool类型来利用线程池。线程池由Common Language Runtime(CLR)进行管理,这意味着每一个CLR只能有一个线程池实例。ThreadPool类型有一个“QueueUserWorkItem”静态方法,这个静态方法接收一个委托,该委托代表一个用户自定义的异步操作。当这个方法被调用时,这个委托就进入内部队列,这个时候,如果线程池中没有线程,则会创建一个新的工作线程,然后将这个委托(第一个)放入队列中。
如果先前的操作执行完毕后,我们又放置了一个新的操作到线程池,那么我们可能会重用上一次操作的那个工作线程。如果我们放置新的操作的时候,线程池中的线程数已达到上限,那么新的操作会在队列中等待,直到线程池中有可用工作线程为止。
需要注意的是,我们尽量在线程池中放置一些需要花费较少时间既能完成的操作,而不要放置需要花费大量时间才能完成的操作,同时不要阻塞工作线程。如果不是这样,工作线程会变得非常繁忙,以至于不能响应用户操作,同时也会导致性能问题以及难以调试的错误。
另外,线程池中的工作线程都是后台线程,这意味着当所有的前台线程执行完毕后,后台线程会被停止执行。
在这一篇中,我们将学习如何使用线程池执行异步操作、如何取消一个操作以及如何防止长时间运行一个线程。
一、在线程池中调用委托
在这一小节中,我们将学习如何在线程池中异步执行一个委托。为了演示如何在线程池中调用一个委托,执行以下操作步骤:
1、使用Visual Studio 2015创建一个新的控制台应用程序。
2、双击打开“Program.cs”文件,编写代码如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 using static System.Threading.Thread; 5 6 namespace Recipe01 7 { 8 class Program 9 { 10 private delegate string RunOnThreadPool(out int threadId); 11 12 private static string Test(out int threadId) 13 { 14 WriteLine("Starting..."); 15 WriteLine($"Is thread pool thread: {CurrentThread.IsThreadPoolThread}"); 16 Sleep(TimeSpan.FromSeconds(2)); 17 threadId = CurrentThread.ManagedThreadId; 18 return $"Thread pool worker thread id was : {threadId}"; 19 } 20 21 private static void Callback(IAsyncResult ar) 22 { 23 WriteLine("Starting a callback..."); 24 WriteLine($"State passed to a callback: {ar.AsyncState}"); 25 WriteLine($"Is thread pool thread: {CurrentThread.IsThreadPoolThread}"); 26 WriteLine($"Thread pool worker thread id: {CurrentThread.ManagedThreadId}"); 27 } 28 29 static void Main(string[] args) 30 { 31 int threadId = 0; 32 var t = new Thread(() => Test(out threadId)); 33 t.Start(); 34 t.Join(); 35 WriteLine($"Thread id: {threadId}"); 36 37 RunOnThreadPool poolDelegate = Test; 38 IAsyncResult r = poolDelegate.BeginInvoke(out threadId, Callback, "a delegate asynchronous call"); 39 r.AsyncWaitHandle.WaitOne(); 40 string result = poolDelegate.EndInvoke(out threadId, r); 41 WriteLine($"Thread pool worker thread id: {threadId}"); 42 WriteLine(result); 43 44 Sleep(TimeSpan.FromSeconds(2)); 45 } 46 } 47 }
3、运行该控制台应用程序,运行效果如下图所示:
在第32行代码处,我们使用老办法创建了一个线程,然后启动它,并等待它执行完毕。因为thread的构造方法只接收不带返回值的委托方法,因此,我们给它传递一个lambda表达式,在该表达式中我们调用了“Test”方法。在“Test”方法中,我们使用“Thread.CurrentThread.IsThreadPoolThread”属性值来判断线程是不是来自线程池。我们还使用“CurrentThread.ManagedThreadId”属性值打印出运行当前代码的线程ID。
在第10行代码处,我们定义了一个委托,该委托表示的方法的返回值为字符串类型,并且接收一个整型类型的输出参数。
在第37行代码处,我们将Test方法赋值给poolDelegate委托,并在第38行代码处,使用委托的“BeginInvoke”方法运行该委托指向的方法(Test)。“BeginInvoke”接收一个回调方法,该方法将在异步操作完成之后被调用。“BeginInvoke”的第三个参数是传递给回调方法的一个用户自定义的状态。通常使用这个状态来分辨一个异步调用。我们使用“IAsyncResult”接口来保存“BeginInvoke”方法的返回值。
“BeginInvoke”方法立即返回,这允许我们可以在线程池中的工作线程执行的同时,继续执行调用“BeginInvoke”方法的线程中的下一条代码。
在第40行代码处,我们可以使用“BeginInvoke”方法的返回值以及对“EndInvoke”方法的调用来获得异步操作的结果。
注意,第39行代码不是必须的,如果我们注释掉这一行代码,程序仍然运行成功,这是因为“EndInvoke”方法会一直等待异步操作完成。调用“EndInvoke”方法的另一个好处是在工作线程中任何未处理的异常都会抛给调用线程。
如果我们注释掉第44行代码,回调方法“Callback”将不会被执行,这是因为主线程已经结束,所有的后台线程都会被停止。
二、在线程池中执行异步操作
在这一小节中,我们将学习如何在线程池中执行异步操作,具体步骤如下:
1、使用Visual Studio 2015创建一个新的控制台应用程序。
2、双击打开“Program.cs”文件,编写代码如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 using static System.Threading.Thread; 5 6 namespace Recipe02 7 { 8 class Program 9 { 10 private static void AsyncOperation(object state) 11 { 12 WriteLine($"Operation state: {state ?? "(null)"}"); 13 WriteLine($"Worker thread id: {CurrentThread.ManagedThreadId}"); 14 Sleep(TimeSpan.FromSeconds(2)); 15 } 16 17 static void Main(string[] args) 18 { 19 const int x = 1; 20 const int y = 2; 21 const string lambdaState = "lambda state 2"; 22 23 ThreadPool.QueueUserWorkItem(AsyncOperation); 24 Sleep(TimeSpan.FromSeconds(2)); 25 26 ThreadPool.QueueUserWorkItem(AsyncOperation, "async state"); 27 Sleep(TimeSpan.FromSeconds(2)); 28 29 ThreadPool.QueueUserWorkItem(state => 30 { 31 WriteLine($"Operation state: {state}"); 32 WriteLine($"Worker thread id: {CurrentThread.ManagedThreadId}"); 33 Sleep(TimeSpan.FromSeconds(2)); 34 }, "lambda state"); 35 36 ThreadPool.QueueUserWorkItem(state => 37 { 38 WriteLine($"Operation state: {x + y}, {lambdaState}"); 39 WriteLine($"Worker thread id: {CurrentThread.ManagedThreadId}"); 40 Sleep(TimeSpan.FromSeconds(2)); 41 }, "lambda state"); 42 43 Sleep(TimeSpan.FromSeconds(2)); 44 } 45 } 46 }
3、运行该控制台应用程序,运行效果(每次运行效果可能不同)如下图所示:
在第10~15行代码处,我们定义了一个带有object类型参数的“AsyncOperation”方法,然后在第23行代码处,我们使用ThreadPool的“QueueUserWorkItem”静态方法在线程池中执行“AsyncOperation”方法。
在第26行代码处,我们又一次使用了“QueueUserWorkItem”静态方法在线程池中执行“AsyncOperation”方法,只不过这次我们给“AsyncOperation”方法传递了state参数。
在第24行和第27行代码处,我们让主线程阻塞2秒钟,以重用线程池中的工作线程。如果我们注释掉这两行代码,那么工作线程的线程ID大部分情况先将会不一样。
在第29~41行代码中,我们演示了如何使用lambda表达式来进行线程池中的异步操作,请自行分析结果。