C#多线程编程(二)线程池与TPL

一、直接使用线程的问题

  1. 每次都要创建Thread对象,并向操作系统申请创建一个线程,这是需要耗费CPU时间和内存资源的。

  2. 无法直接获取线程函数返回值

  3. 无法直接捕捉线程函数内发生的异常

使用线程池可以解决第一个问题

二、.NET中的线程池

在这里只简单的介绍一下ThreadPool,由于TPL的存在,我工作中大部分使用的是TPL中的类,这是后面介绍的重点。

1. ThreadPool.QueueUserWorkItem

这个方法有三个重载

  • public static bool QueueUserWorkItem(WaitCallback callBack)

  • public static bool QueueUserWorkItem(WaitCallback callBack, object? state)

  • public static bool QueueUserWorkItem<TState>(Action<TState> callBack, TState state, bool preferLocal)

第一个函数需要传入一个WaitCallback 委托,该委托的定义如下

  • public delegate void WaitCallback(object? state);

第二个函数多了一个state参数,表示需要传给委托的参数,若无需传参调用第一个函数即可。

第三个函数是一个泛型版本,还多了一个布尔类型的参数preferLocal,这个参数表示传入的委托将会在放入线程池工作线程的本地队列还是线程池的全局队列。

线程池内部有本地队列和全局队列的概念,线程池遵循生产者-消费者模式,线程池还可以为线程数量提供良好的伸缩性,有关.NET线程池的详细信息,请参见https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.threadpool?view=netcore-3.1

需要注意的是,线程池中的线程默认为后台线程,这意味着如下代码一般不能按预期工作。

1 static void Main(string[] args)2 {3      ThreadPool.QueueUserWorkItem(state =>4      {5          Thread.Sleep(100);6          Console.WriteLine("执行完毕");7      });8 }

并且由于线程会被复用,所以不能作依赖于某个特定线程的操作。

2. 何时不使用线程池(摘录微软文档)

  1. 需要一个前台线程。

  2. 需要具有特定优先级的线程。

  3. 拥有会导致线程长时间阻塞的任务。 线程池具有最大线程数,因此大量被阻塞的线程池线程可能会阻止任务启动。

  4. 需将线程放入单线程单元。 所有 ThreadPool 线程均位于多线程单元中。

  5. 需具有与线程关联的稳定标识,或需将一个线程专用于一项任务。

三、更方便的解决方案(使用TPL)

1.处理线程池未解决的问题

线程池虽然解决了线程资源浪费的问题,但是以下两点还未解决

  1. 无法直接获取线程函数返回值

  2. 无法直接捕捉线程函数内发生的异常

在上一篇中MyTask类可以解决这两个问题,但由于内部是每个Task直接开一个线程,资源浪费的问题还是没有解决,所以我们是不是能够把两者结合呢?

2.Task初体验

无返回值无异常

1 static void Main(string[] args) 2 { 3      Task task = new Task(() =>  4      { 5          Thread.Sleep(100); 6          Console.WriteLine($"是否是线程池线程:{Thread.CurrentThread.IsThreadPoolThread}"); 7      }); 8      task.Start(); 9      try10      {11          task.Wait();12      }13      catch(Exception e)14      {15          Console.WriteLine(e.Message);16      }17 }

输出如下:

无返回值有异常

1 static void Main(string[] args) 2 { 3      var task = new Task(() =>  4      { 5          Console.WriteLine($"是否是线程池线程:{Thread.CurrentThread.IsThreadPoolThread}"); 6          var task1 = new Task(() =>  7          { 8              Console.WriteLine($"是否是线程池线程:{Thread.CurrentThread.IsThreadPoolThread}"); 9              throw new Exception("延续任务发生异常");10          },TaskCreationOptions.AttachedToParent);11          task1.Start();12          throw new Exception("主任务发生异常");13      });14 15     task.Start();16      try17      {18          task.Wait();19      }20      catch(AggregateException ae)//Task内部包装了异常,有异常发生Wait()内部会抛出一个聚合异常21      {22          foreach(var e in ae.Flatten().InnerExceptions)//把阶梯式的聚合异常变为扁平的异常23          {24              Console.WriteLine(e.Message);25          }26      }27 }

有返回值的就不演示了。可以看到,使用Task解决了开始的三种问题,但事物总是具有两面性,有优点也有缺点,Task会带来额外的内存分配,Task抽象层次过高,深入理解并使用好并非易事,在与async/await关键字配合编写异步代码时更加突出。

网上关于Task的使用例子很多,微软文档也很全,我在此这里补充一下需要注意的地方。

  • 任务是托管线程上更高层次的抽象

  • 任务的执行由任务调度器(TaskScheduler)决定

  • 默认的任务调度器是线程池调度器,它使用线程池执行任务

  • Task.Run()静态方法和Task.Start()实例方法以及默认的任务工厂Task.Factory都是使用默认的线程池任务调度器

  • 因此说Task是对线程池的封装是不准确的

  • 通过指定TaskCreationOptions.LongRunning枚举便可让任务在非线程池中的线程上执行,这样可以避免长期占用线程池中的线程,因为线程池是有大小的,一般线程池用来处理简单但量多的工作。

(0)

相关推荐

  • 线程(一)——线程,线程池,Task概念+代码实践

    目录 线程(一)--线程,线程池,Task概念+代码实践 12.1 通过TPL进入线程池 12.2 不同过TPL进入线程池 12.3 线程池优化 12.1.1 Task异常捕获 12.2.1 Queu ...

  • 多线程之旅(ThreadPool 线程池)

    一.什么是ThreadPool 线程池(源码) 1.线程池顾名思义,有我们的系统创建一个容器装载着我们的线程,由CLR控制的所有AppDomain共享.线程池可用于执行任务.发送工作项.处理异步 I/ ...

  • python爬虫14 | 就这么说吧,如果你不懂多线程和线程池,那就去河边摸鱼!

    你知道吗? 在我的心里 你是多么的重要 就像 恩 请允许我来一段 freestyle 你们准备好了妹油 你看 这个碗 它又大又圆 就像 这条面 它又长又宽 你们 在这里 看文章 觉得 很开心 就像 我 ...

  • 带你通俗易懂的理解——线程、多线程与线程池

    进程与线程 进程:进程就是正在执行的程序. 线程:是程序执行的一条路径, 一个进程中可以包含多条线程. 通俗理解:例如你打开微信就是打开一个进程,在微信里面和好友视频聊天就是开启了一条线程. 两者之间 ...

  • 快速掌握并发编程---线程池的原理和实战

    池 上图是装水的池子--水池. 流行池化技术,那么到底什么是池化技术呢? 池化技术简单点来说,就是提前保存大量的资源,以备不时之需.在机器资源有限的情况下,使用池化技术可以大大的提高资源的利用率,提升 ...

  • 如何合理地估算线程池大小?

    这个问题虽然看起来很小,却并不那么容易回答. 大家如果有更好的方法欢迎赐教,先来一个天真的估算方法: 假设要求一个系统的TPS(Transaction Per Second或者Task Per Sec ...

  • Java并发编程之线程的创建

    简介 线程是基本的调度单位,它被包含在进程之中,是进程中的实际运作单位,它本身是不会独立存在.一个进程至少有一个线程,进程中的多个线程共享进程的资源. Java中创建线程的方式有多种如继承Thread ...

  • C#线程学习笔记三:线程池中的I/O线程

    本笔记摘抄自:https://www.cnblogs.com/zhili/archive/2012/07/20/MultiThreads.html,记录一下学习过程以备后续查用.     一.I/O线 ...

  • Java并发多线程编程——Volatile原理与使用

    优质文章,第一时间送达 76套java从入门到精通实战课程分享 一.volitile的理解 Volatile称之为轻量级锁,被volatile修饰的变量,在线程之间是可见的. 可见即一个线程修改了这个 ...

  • Java主线程等待子线程、线程池

    print public class TestThread extends Thread { public void run() { System.out.println(this.getName() ...