线程中task取消_.NET 复习笔记 / TPL,线程/并发/异步

a07d0f4c41d7391f55670b3dc15a7aa2.png

通常我们会在下面两种场景中用到多线程,

  • 性能,当我们面对很多处理工作,并且你的CPU拥有多核心的时候,我们就可以通过同时处理多个操作来提高性能。
  • 可响应性,在GUI客户端程序中,为了避免用户界面对应的线程出现无法响应的状态,我们需要把一些工作交给另外的线程。

本文内容基于 .net core 2.2

1、CLR线程与操作系统线程

在介绍 TPL 之前,我们需要了解 .NET Core 中的 CLR 线程以及它与操作系统线程(本地线程)的区别。

CLR Threading Overview​github.com
ad38e4f8c9213e419ad6abe94d04a01e.png

我们将 CLR 中的线程称为“托管线程”而托管代码就运行在“托管线程”之上。“托管线程”与本地线程最本质的区别就是:本地线程是一个在物理机器上运行本地代码(native code)的线程而托管线程则是运行在CLR上的虚拟线程。

就像JIT编译器将"虚拟"的IL指令映射到本地指令在物理机器上执行一样,CLR的线程基础架构也将“虚拟”的托管线程映射到由操作系统提供的本地线程。

在任何时候,一个托管线程可能分配也可能没有分配到一个本地线程。比如一个托管线程创建(通过 "new System.Threading.Thread") 但还没有启动(Thread.Start)的时候,一个托管线程就并没有分配到本地线程。类似的,一个托管线程可以,原则上,在执行过程中在多个本地线程之间转移,不过目前实际上 CLR不支持这一点。

托管线程的编程接口刻意的隐藏了一些本地线程的信息,这是因为:

  • 托管线程并不是时刻都需要映射到一个本地线程(甚至不一定映射到一个本地线程)。
  • 不同操作系统的本地线程抽象是不一样的。
  • 原则上,托管线程是“虚拟的”。

CLR为托管线程提供等效的抽象,由CLR本身实现。例如,它不暴露操作系统线程的本地存储 (TLS) 机制, 而是提供托管的 "thread-static" 变量。同样, 它也不暴露本机线程的 "线程ID",而是提供独立于操作系统生成的 "托管线程ID"。但是,出于诊断调试的目的,你可以通过 System.Diagnostics 命名空间中的类型来获取本地线程的某些详细信息。

除了上面所说的之外,托管线程同时也需要一些本机线程通常不需要的功能:首先,托管线程在其堆栈上保存 GC 引用, 因此 CLR 必须能够在每次发生 GC 时枚举 (也可能是修改) 这些引用。为此, CLR 必须 "挂起" 每个托管线程 (在所有 GC 引用都可以被找到时将线程停止在某个位置)。其次,在卸载 AppDomain 时, CLR 必须确保没有线程在该 AppDomain 中执行代码。这需要强制线程从该 AppDomain 中脱离的能力,CLR 通过向此类线程中注入ThreadAbortException 来实现此目的。

2、TPL与ThreadPool(线程池)

TPL 全称 Task Parallel Library,是 .NET Framework 与 .NET Core“标准库”的一部分,在 .NET Framework 4 中加入。位于 System.Threading 和 System.Threading.Tasks 命名空间。TPL 的目的是让开发者以更简单、更具生产力的方式来为应用程序添加并发性。

TPL 会动态地扩展并发程度,以便最有效地使用所有可用的处理器。另外 TPL 还支持: 分批处理工作、ThreadPool 上的线程调度、取消-支持、状态管理和其他低级别的细节处理。通过使用 TPL 你可以最大程度地提高代码的性能,并且同时只需专注于程序的业务功能。

TPL 的多线程是基于线程池(ThreadPool)的,这是因为创建和销毁线程是相对“昂贵”的操作,并且太多的线程会浪费内存资源,另外操作系统为调度可运行的线程必须执行上下文切换,所以太多的线程对性能是不利的(这里的太多的线程是相对于你的电脑的核心数量的)。为了改善这个情况,CLR 包含了管理自己线程池(thread pool)的代码,我们可以通过ThreadPool.QueueUserWorkItem方法向ThreadPool添加一个“工作项”(WorkItem)。当你向线程池添加多个工作项时,线程池会尝试只用一个线程来服务所有请求。然而,如果你发出的请求的速度超过了线程池线程处理它们的速度,就会创建额外的线程。最终,应用程序的所有请求能由尽可能少量的线程处理。而当线程在完成任务后并闲置一段时间后,线程会“醒来”终止自己以释放资源。

3、TPL核心类型-Task类

由于上面提到的QueueUserWorkItem不够“强大”(无法获取执行状态与返回结果),为此Microsoft引入了任务(Task)的概念,我们可以通过System.Threading.Tasks命名空间中的Task类型来使用任务,下面的示例代码是使用Task类型来实现QueueUserWorkItem的功能。

//ThreadPool方式

Task类是TPL的核心类型,Task与Task<TResult>两个类分别代表了没有返回值和有返回值的任务,构造一个Task需要一个Action或Action<TResult>。Task是一个任务的抽象而Action就是这个任务具体执行的内容,也可以理解为Task是对一个Action的封装并在Action的基础上提供了一系列任务相关功能,有了它你就不需要关注线程,并且可以获得任务的状态、控制Task的执行以及利用各种TaskScheduler来调度Task等等。

下面的示例代码演示了如何利用Task来异步执行一个现有的方法。

//一个求和方法

使用上面的方法来初始化一个Task并执行获得结果。

//创建一个Task(此时还未开始运行)

在上例中,当线程调用Wait方法时,系统会检查Task是否已开始执行。如果是,调用Wait的线程就会阻塞,直到Task运行结束为止。但如果Task还没开始执行,系统可能(取决于TaskScheduler)使用调用Wait的线程来执行Task。在这种情况下,调用Wait的线程就不会阻塞;它会执行Task并立即返回。好处在于没有线程会被阻塞,所以减少了对资源的占用(因为不需要创建一个线程来替代被阻塞的线程),并提升了性能。不好的地方在于,假如线程在调用Wait前已获得了一个线程同步锁,而Task试图获取同一个锁,就会造成死锁的线程。

Task的主要功能及相关类型:

  • 使用CancellationToken可控制Task在被调度前取消执行。
  • 使用TaskCreationOptions控制Task的创建与执行方式。
  • 在任务完成时使用Task.ContinueWith()来启动新的任务 。
  • 使用任务工厂(TaskFactory)创建一组共享配置的任务。
  • 任务调度器(TaskScheduler)用于调度任务的执行,微软在Parallel Extensions Extras包中提供了多个用于不同场景的任务调度器。

限于篇幅这里就不再深入,感兴趣的读者可以查看下面的文档。

Task Class (System.Threading.Tasks)​docs.microsoft.com
c61bf5201f1a95576a95df5a4d55e973.png

另外Task通常会和await以及async这两个关键字结合使用来实现异步编程,这会在下文中介绍。

4、TPL关键类型-Parallel类

Parallel类可以极大的简化并发编程。

假如我们有一个很大的集合需要对里面的每一项做一个操作,用单线程的方式的代码可能是这样的:

// 顺序处理           

传统多线程处理:

// 传统并发处理,这里做两个示例:

TPL 并行处理:

// 并行处理

我相信大部分人都认可无论从代码量和代码的可读性方面来说TPL的处理方式都是非常好的,简单,直观,不需关注线程处理细节。

Parallel包括下面这三个静态方法:

  • Parallel.For //并发执行指定Action,并以for循环中的index作为参数。
  • Parallel.ForEach //并发的执行指定的Action,并以集合中的每个元素作为参数。
  • Parallel.Invoke //并发的方式执行参数中传入的Action数组。

三个方法的示例代码:

//使用Parallel.For并发来计算一个目录下所有文件体积总和

是不是用起来很方便啊!

5、PLINQ

PLNQ即Parallel-LINQ,相对于之前LINQ的顺序查询PLNQ的查询是并行的,也就是并发的LINQ,它可以利用多线程的优势在处理大集合的时候提高性能。PLNQ主要由下面两个类实现。

  • ParallelQuery
  • ParallelEnumerable

其中ParallelQuery 是并发查询的集合对象,而ParallelEnumerable包含了对ParallelQuery的LINQ扩展方法。

PLNQ执行流程图:

21f04438dc54dfc997ac5903d13d35f0.png
PLINQ执行流程

你可以利用集合的 AsParallel() 方法(这其实是定义在ParallelEnumerable中的扩展方法)来将一个集合转化为一个支持 PLNQ 的 ParallelQuery 对象以使用PLNQ。

示例代码:

//从一个大集合中查找所有的偶数

在PLNQ的使用中你可以使用CancellationToken以及ParallelExecutionMode,ParallelMergeOptions来控制并发查询的处理方式。

需要注意的是使用PLINQ并不能在任何情况保证性能的提高,不恰当的使用甚至可能导致性能的下降,关于这方面的更多内容请查看下面的官方文档,

Introduction to PLINQ​docs.microsoft.com
bd2d52f32a60c8c386282711c37779f4.png

6、TAP异步编程与async、await

简单的说异步编程的目的就是为了在不阻塞当前线程的前提下完成由当前线程发起的任务,并且在大多数情况下需要任务能返回到当前线程。

TAP(Task asynchronous programming model)是基于Task的异步编程模型,它利用TPL中的Task类型以及async与await关键字实现异步编程。下面是微软文档中对TAP的描述:

The Task asynchronous programming model (TAP) provides an abstraction over asynchronous code. You write code as a sequence of statements, just like always. You can read that code as though each statement completes before the next begins. The compiler performs a number of transformations because some of those statements may start work and return a Task that represents the ongoing work.
Task 异步编程模型 (TAP) 提供了异步代码的抽象。使你在编写异步代码的时候仍然可以按照顺序书写代码语句并按照顺序阅读你的代码,也就是说每一行代码都会在上一行代码执行完毕后执行。但是编译器会自动执行一系列转换使代码能执行并返回一个代表异步执行的任务(Task)。

在 TPL 加入之前 .NET 里的异步编程通常是基于事件的 既 EAP(Event-based Asynchronous Pattern),关于 EAP 的详细内容可查看下面的文档,这里我们主要介绍 TAP。

Event-based Asynchronous Pattern (EAP)​docs.microsoft.com

TAP除了关键的Task类型,还需要两个关键字async与await, 其中async是方法修饰符,用于修饰一个方法、lambda表达式或者匿名方法来表示它们是异步的,如果方法或表达式上使用此修饰符则可称之为异步方法。 await则是操作符,应用于异步方法内的Task对象(必须在async修饰的方法内使用await),以在方法的执行中插入挂起点,等待任务的完成。

下面我们使用Task来渐进的实现异步编程:

1、首先我们有一个任务是计算两个数的乘积(假设它是个很耗时间的任务 )。

public 

2、然后我们用Task来改造这个方法,将它改造成一个异步方法。这里使用了Task.Run方法来运行乘法计算,这个方法是异步的并且会返回一个Task<int>对象。泛型类型int表示了这个Task的返回类型,并且我们在方法名后面加上Async来表明这是一个异步方法。

public 

3、异步方法定义好后我们就可以使用了。

//注意:await关键字必须在async修饰的方法内使用

这里,Run所处的线程在执行到await时就会挂起(但并不会阻塞该线程执行其他的任务,如果这是UI线程它就可以继续接收用户操作事件并进行处理),等待对应的任务完成后返回并继续执行下面的代码。这就是TAP要达到的目的:

That's the goal of this syntax: enable code that reads like a sequence of statements, but executes in a much more complicated order based on external resource allocation and when tasks complete. It's analogous to how people give instructions for processes that include asynchronous tasks.
这就是此语法的目标:能够用顺序的方式阅读代码语句,但会根据外部资源分配和任务完成时间以更复杂的顺序执行。它类似于我们人类对包含异步任务的工作提供的指令。

简单的说TAP就是用同步方式来编写异步的代码,它不仅编写方便而且代码的可读性也非常好,在需要大量异步代码的场景是非常有帮助的。

更多相关内容请参考官方文档,

Asynchronous programming in C#​docs.microsoft.com
c61bf5201f1a95576a95df5a4d55e973.png
Task-based Asynchronous Pattern (TAP)​docs.microsoft.com

7、结语

关于异步和并发编程的内容还有很多,包括各种场景的锁、IO密集与计算密集的处理以及线程的调度等等。本篇只是从TPL使用的角度抛砖引玉的介绍了一点 .NET 多线程编程的知识,希望能对你有所帮助。有任何错误和不严谨的地方烦请在评论中指出,非常感谢!

另外附上本文参考的其他一些相关文档的链接,

Task Parallel Library (TPL)​docs.microsoft.com await 关键字 - C# Reference​docs.microsoft.com async 关键字- C# Reference​docs.microsoft.com await使用中的阻塞和并发 - 楼上那个蜀黍 - 博客园​www.cnblogs.com
03dfe3b6f93ebda663acbd22b5e9b52e.png

另参考了《CLR via C#》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值