【C#】ThreadPool与Task

it2023-01-08  65

1. 线程池(ThreadPool)

为什么要使用线程池? 主要原因是创建和销毁一个线程的代价是昂贵的,会消耗较多的系统资源;线程池原理? 每个CLR只有一个线程池,线程池线程不是在CLR初始化时自动创建的,而是向线程池派发(dispatch)异步操作时,如果线程池中没有线程,则会创建一个新新线程,不同的是,这个线程不会被销毁,执行完后进入空闲状态,等待响应新的异步请求。 当然,如果一个线程池线程不做任何事情,也是一种资源浪费。所以,当一个线程空闲特定一段时间后,会自己醒来终止自己以释放资源。 请注意线程池中的线程都是后台线程,在所有的前台线程运行结束后,所有的后台线程将停止工作。 创建一个线程时,会将当前线程的上下文传递给新建线程,而收集和复制上下文信息会耗费一定的时间和性能,在不需要传递上下文的场景中,可以通过System.Threading.ExecutionContext类型中的SuppressFlow()方法和RestoreFlow()方法分别阻止和恢复上下文的传递或流动。使用线程池时的注意项? 线程池不适合需要长时间运行的作业,或者处理需要与其它线程同步的作业;避免在线程池中执行I/O首先的操作,这种任务应该使用TPL模型;不要手动设置线程池的最小和最大线程数,CLR会自动执行线程池的扩张和收缩,手动干预会使性能更差(目前默认是1000个线程); 线程池的两种使用方式: 通过异步编程模型(Asynchronous Programming Model,简称APM)展示怎样在线程池中异步的执行委托? 下面的方式为异步编程模型(这是.net历史中第一个异步编程模式),这里使用委托的BeginInvoke()方法来来运行该委托,BeginInvoke接收一个回调函数,会在任务执行完后被调用;现在这种APM编程模型使用的越来越少了,更多的是使用任务并行库(Task Parallel Library, 简称TPL)。 private delegate string RunOnThreadPool(out int threadId);//委托 static void Main(string[] args) { //使用APM方式 进行异步调用 异步调用会使用线程池中的线程 IAsyncResult r = poolDelegate.BeginInvoke(out threadId, Callback, "委托异步调用"); r.AsyncWaitHandle.WaitOne(); // 获取异步调用结果 string result = poolDelegate.EndInvoke(out threadId, r); } 通过ThreadPool.QueueUserWorkItem()向线程池中放入异步操作? static void Main(string[] args) { //直接将方法传递给线程池,AsyncOperation为要异步执行的方法 ThreadPool.QueueUserWorkItem(AsyncOperation); //直接将方法传递给线程池 并且通过state传递参数 ThreadPool.QueueUserWorkItem(AsyncOperation, "async state"); //使用Lambda表达式将任务传递给线程池 并且通过 state传递参数 ThreadPool.QueueUserWorkItem(state => { WriteLine($"Operation state: {state}"); WriteLine($"工作线程 id: {CurrentThread.ManagedThreadId}"); }, "lambda state"); } private static void AsyncOperation(object state) { WriteLine($"Operation state: {state ?? "(null)"}"); WriteLine($"工作线程 id: {CurrentThread.ManagedThreadId}"); } 使用普通创建线程方式和线程池方式有何区别? 分别运行下面两个方法,其中普通线程执行了2s多,但是创建了500个线程,线程池执行了9s多,但是只创建了很少的线程,为操作系统节省了线程和内存空间,但是花费的时间较多; static void UseThreads() { for (int i = 0; i < 500; i++) { var thread = new Thread(() => { Sleep(TimeSpan.FromSeconds(0.1)); }); thread.Start(); } } static void UseThreadPool() { for (int i = 0; i < 500; i++) { ThreadPool.QueueUserWorkItem(c => { Sleep(TimeSpan.FromSeconds(0.1)); }); } } 如何通过CancellationTokenSource取消线程: private CancellationTokenSource cts = new CancellationTokenSource(); private void StartThread(object sender, RoutedEventArgs e) { ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 100)); } private void Count(CancellationToken token, Int32 countTo) { for (Int32 count = 0; count < countTo; count++) { if (token.IsCancellationRequested) { this.Dispatcher.Invoke(() => { this.AutoAddTextBox.Text += '\n' + "Count is canceled" + '\n'; }); break; } this.Dispatcher.Invoke(() => { this.AutoAddTextBox.Text += count.ToString() + " "; }); Thread.Sleep(100); } this.Dispatcher.Invoke(() => { this.AutoAddTextBox.Text += "Count is Done!" + '\n'; }); } private void CancelThread(object sender, RoutedEventArgs e) { //执行完Cancel()方法后,会将IsCancellationRequested置为true cts.Cancel(); }

当运行到第32次时取消线程。

BackgroundWorker组件介绍 BackgroundWorker是基于事件的异步编程模式( Event-based Asynchronous Pattern,简称EAP),是.net历史上第二种用来构造异步程序的方式,现在推荐使用TPL;该组件被应用于WPF中,通过它实现的代码可以直接与UI控制器交互;

2、任务(Task)

什么是任务并行库(TPL)? 为了实现线程的同步、异步、异常传递等问题,需要编写较多的代码,来达到正确性和健壮性,而且最大的问题是没有内建的机制让你知道操作在什么时候完成,也没有机制在操作完成时获取返回值。为此,.NET 4.0引入了一个关于异步操作的API——任务并行库(TPL)。TPL的内部使用了线程池,而且效率更高。在把线程归还到线程池之前,它会在同一个线程中顺序执行多个任务,减少任务上下文切换带来的时间浪费问题。其中,任务(Task)是对象,封装了要异步执行的操作。 TPL被认为是线程池之上的又一抽象层,其对程序员隐藏了与线程池交互的底层代码,并提供了更方便的细粒度API,TPL的核心概念是任务Task。Task创建的是线程池任务,Thread默认创建的是前台线程;线程池一般只运行执行时间较短的异步操作;新建Task的方法有3种,示例如下: 其中Task2中使用的是Run()静态方法,Task4中设置了LongRunning,表明需要长时间运行,因此不是线程池线程;注意,Task.Run方法只是Task.Factory.StartNew的一个快捷方式,但是后者有附加的选项; public Main() { var task1 = new Task(() => TaskMethod("Task 1")); task1.Start(); Task.Run(() => TaskMethod("Task 2")); Task.Factory.StartNew(() => TaskMethod("Task 3")); Task.Factory.StartNew(() => TaskMethod("Task 4"), TaskCreationOptions.LongRunning); } private void TaskMethod(string name) { string str = string.Format("{0} 线程id:{1},线程池中线程:{2}", name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread); this.Dispatcher.BeginInvoke(new Action(delegate { this.textbox.Text += str + "\n"; })); }

输出结果:

Task 1 线程id:10,线程池中线程:True Task 2 线程id:12,线程池中线程:True Task 3 线程id:13,线程池中线程:True Task 4 线程id:12,线程池中线程:False Task的基本操作: // 主线程直接执行操作 TaskMethod("主线程任务"); // 访问 Result属性,得到运行结果 Task<int> task = CreateTask("Task 1"); task.Start(); task.Wait();//显式等待任务完成后,才执行后面的代码 int result = task.Result;//task.Result是隐式等待任务完成后,才执行后面的代码,即在任务尚未完成时查询任务的Result //这里Wait和Result是等待单个任务完成,会阻塞当前线程,如果要等待一个Task对象数据,则采用WaitAll()或WaitAny() WriteLine($"运算结果: {result}"); // 使用当前线程,同步执行任务 task = CreateTask("Task 2"); task.RunSynchronously();//这里是运行在主线程上,非线程池线程 result = task.Result; WriteLine($"运算结果:{result}");

一个伸缩性好的程序不应该使线程阻塞,当调用Wait,查询任务的Result属性时,极有可能造成线程池创建新线程,增大的资源的消耗。ContinueWith方法可以在任务完成时执行另一个任务,避免线程的阻塞。

private void StartThread(object sender, RoutedEventArgs e) { Task<int> t = new Task<int>(n => Sum((int)n), 1000); t.Start(); Console.WriteLine("before result out"); Task cwt = t.ContinueWith(task => Console.WriteLine("result is : " + t.Result)); //ContinueWith()方法返回一个Task对象,但该对象一般不用,可直接忽略掉 Console.WriteLine("after result out"); } private int Sum(int countTo) { int sum = 0; for (int i = 0; i < countTo; i++) { sum += i; } return sum; } 输出结果: before result out after result out result is : 499500

任务可以启动子任务,且只有当各子任务全部执行结束,父任务才结束:

Task<int[]> parent = new Task<int[]>(() => { var results = new int[3]; new Task(() => results[0] = Sum(500), TaskCreationOptions.AttachedToParent).Start(); new Task(() => results[1] = Sum(1000), TaskCreationOptions.AttachedToParent).Start(); new Task(() => results[2] = Sum(1500), TaskCreationOptions.AttachedToParent).Start(); return results; }); //parent.ContinueWith(parentTask => Array.ForEach(parentTask.Result, Console.WriteLine)); parent.ContinueWith(parentTask => { foreach (var item in parent.Result) { Console.WriteLine(item.ToString()); } }); parent.Start(); 输出结果: 124750 499500 1124250

Task相对于ThreadPool.QueueUserWorkItem具有很多附加属性,如任务状态,父任务的引用,TaskScheduler的引用,回调方法的引用等等,但会增加代价,因为需要为所有这些属性分配内存,所以在不需要这些附加功能时,采用ThreadPool.QueueUserWorkItem能获得更好的资源利用率。

System.Threading的Timer类 该类使一个线程池线线程定时调用一个方法。在内部,线程池为所有的Timer对象只使用了一个线程,即单独有一个线程控件所有Timer对象中回调方法的调用,当一个Timer对象到期时,该线程会在内部调用ThreadPool.QueueUserWorkItem,将回调任务添加到线程池队列中。当回调方法执行时间很长,而Timer间隔时间又很短时,会存在线程池多个线程同时执行一个回调方法。 所以要在一个线程池线程上执行定期性发生的后台任务时,采用Timer定时器。 System.Windows.Forms的Timer类及System.Windows.Threading的Dispatcher类的功能相同,与上面定时器的区别是:回调方法只由一个线程完成,就是设置定时器的线程,即在一个线程中设置了DispatcherTimer,那么其回调方法也只在该线程中执行。Task类中几个常用方法: public static bool WaitAll(Task[] tasks):判断tasks是否全部执行完毕; public static bool WaitAny(Task[] tasks):判断tasks中是否存在执行完毕的task; public static Task WhenAll(Task[] tasks):当所有tasks执行完毕后,创建并返回一个新的Task; public static Task WhenAll(Task[] tasks):当tasks中存在已执行完毕的task,创建并返回一个新的Task;TaskScheduler是一个非常重要的抽象,该组件实际上负责如何执行任务,默认的任务调试器将任务放置到线程池的工作线程中,这是TPL的默认选项;. C#5.0引入了新的语言特性,称为异步函数(asynchronous function),它是TPL之上更高级别的抽象,真正简化了异步编程,主要依靠async和await关键字实现;

参考:C#多线程编程系列 参考:C#多线程总结(纯干货)

最新回复(0)