细说并发编程-TPL


本节导航

  • 基本概念
    • 并发编程
    • TPL
  • 线程基础
    • windows为何要支持线程
    • 线程开销
    • CPU的发展
    • 使用线程的理由
  • 如何写一个简单Parallel.For循环
    • 数据并行
    • Parallel.For剖析

  优秀软件的一个关键特征就是具备并发性。过去的几十年,咱们能够进行并发编程,可是
难度很大。之前,并发性软件的编写、调试和维护都很难,这致使不少开发人员为图省事
放弃了并发编程。新版 .NET 中的程序库和语言特征,已经让并发编程变得简单多了。随
着 Visual Studio 2012 的发布,微软明显下降了并发编程的门槛。之前只有专家才能作并发
编程,而今天,每个开发人员都可以(并且应该)接受并发编程。html

  许多我的电脑和工做站都有多核CPU,能够同时执行多个线程。为了充分利用硬件,您能够将代码并行化,以便跨多个处理器分发工做。数据库

  在过去,并行须要对线程和锁进行低级操做。Visual Studio和.NET框架经过提供运行时、类库类型和诊断工具来加强对并行编程的支持。这些特性是在.NET Framework 4中引入的,它们使得并行编程变得简单。您能够用天然的习惯用法编写高效、细粒度和可伸缩的并行代码,而无需直接处理线程或线程池。编程

  下图展现了.NET框架中并行编程体系结构。windows

使用场景

1 基本概念

1.1 并发编程

  • 并发api

    同时作多件事情数组

  这个解释直接代表了并发的做用。终端用户程序利用并发功能,在输入数据库的同时响应用户输入。服务器应用利用并发,在处理第一个请求的同时响应第二个请求。只要你但愿程序同时作多件事情,你就须要并发。promise

  • 多线程安全

    并发的一种形式,它采用多个线程来执行程序。从字面上看,多线程就是使用多个线程。多线程是并发的一种形式,但不是惟一的形式。服务器

  • 并行处理多线程

    把正在执行的大量的任务分割成小块,分配给多个同时运行的线程。

  为了让处理器的利用效率最大化,并行处理(或并行编程)采用多线程。当现代多核 CPU行大量任务时,若只用一个核执行全部任务,而其余核保持空闲,这显然是不合理的。

  并行处理把任务分割成小块并分配给多个线程,让它们在不一样的核上独立运行。并行处理是多线程的一种,而多线程是并发的一种。

  • 异步编程

    并发的一种形式,它采用 future 模式或回调(callback)机制,以免产生没必要要的线程。

  一个 future(或 promise)类型表明一些即将完成的操做。在 .NET 中,新版 future 类型
有 Task 和 Task 。在老式异步编程 API 中,采用回调或事件(event),而不是
future。异步编程的核心理念是异步操做:启动了的操做将会在一段时间后完成。这个操做
正在执行时,不会阻塞原来的线程。启动了这个操做的线程,能够继续执行其余任务。当
操做完成时,会通知它的future,或者调用回调函数,以便让程序知道操做已经结束。

NOTE:一般状况下,一个并发程序要使用多种技术。大多数程序至少使用了多线程(经过线程池)和异步编程。要大胆地把各类并发编程形式进行混合和匹配,在程序的各个部分使用
合适的工具。

1.2 TPL

  任务并行库(TPL)是System.Threading和System.Threading.Tasks命名空间中的一组公共类型和API。

  TPL动态地扩展并发度,以最有效地使用全部可用的处理器。经过使用TPL,您能够最大限度地提升代码的性能,同时专一于您的代码的业务实现。

  从.NET Framework 4开始,TPL是编写多线程和并行代码的首选方式。

2 线程基础

2.1 Windows 为何要支持线程

  在计算机的早期岁月,操做系统没提供线程的概念。事实上,整个系统只运行着一个执行线程(单线程),其中同时包含操做系统代码和应用程序代码。只用一个执行线程的问题在于,长时间运行的任务会阻止其余任务执行。
例如,在16位Windows的那些日子里,打印一个文档的应用程序很容易“冻结”整个机器,形成OS和其余应用程序中止响应。有的程序含有bug,会形成死循环。遇到这个问题,用户只好重启计算机。用户对此深恶痛绝。

  因而微软下定决心设计一个新的OS,这个OS必须健壮,可靠,易因而伸缩以安全,同同时必须改进16位windows的许多不足。

  微软设计这个OS内核时,他们决定在一个进程(Process)中运行应用程序的每一个实例。进程不过是应用程序的一个实例要使用的资源的一个集合。每一个进程都被赋予一个虚拟地址空间,确保一个进程使用的代码和数据没法由另外一个进程访问。这就确保了应用程序实例的健壮性。因为应用程序破坏不了其余应用程序或者OS自己,因此用户的计算体验变得更好了。

  听起来彷佛不错,但CPU自己呢?若是一个应用程序进入无限循环,会发生什么呢?若是机器中只有一个CPU,它会执行无限循环,不能执行其它任何东西。因此,虽然数据没法被破坏,并且更安全,但系统仍然可能中止响应。微软要修复这个问题,他们拿出的方案就是线程。做为Windows概念,线程的职责是对CPU进行虚拟化。Windows为每一个进程都提供了该进程专用的专用的线程(功能至关于一个CPU,可将线程理解成一个逻辑CPU)。若是应用程序的代码进入无限循环,与那个代码关联的进程会被“冻结”,但其余进程(他们有本身的线程)不会冻结:他们会继续执行!

2.2 线程开销

  线程是一个很是强悍的概念,由于他们使windows即便在执行长时间运行的任务时也能随时响应。另外,线程容许用户使用一个应用程序(好比“任务管理器”)强制终止彷佛冻结的一个应用程序(它也有可能正在执行一个长时间运行的任务)。可是,和一切虚拟化机制同样,线程会产生空间(内存耗用)和时间(运行时的执行性能)上的开销。

  建立线程,让它进驻系统以及最后销毁它都须要空间和时间。另外,还须要讨论一下上下文切换。单CPU的计算机一次只能作一件事情。因此,windows必须在系统中的全部线程(逻辑CPU)之间共享物理CPU。

在任何给定的时刻,Windows只将一个线程分配给一个CPU。那个线程容许运行一个时间片。一旦时间片到期,Windows就上下文切换到另外一个给线程。每次上下文切换都要求Windows执行如下操做:

  • 将CPU寄存器中的值保存到当前正在运行的线程的内核对象内部的一个上下文结构中。
  • 从现有线程集合中选一个线程供调度(切换到的目标线程)。若是该线程由另外一个进程拥有,Window在开始执行任何代码或者接触任何数据以前,还必须切换CPU“看得见”的虚拟地址空间。
  • 将所选上下文结构中的值加载到CPU的寄存器中。

  上下文切换完成后,CPU执行所选的线程,直到它的时间片到期。而后,会发生新一轮的上下文切换。Windows大约每30ms执行一次上下文切换。

  上下文切换是净开销:也就是说上下文切换所产生的开销不会换来任何内存或性能上的收益。

  根据上述讨论,咱们的结论是必须尽量地避免使用线程,由于他们要耗用大量的内存,并且须要至关多的时间来建立,销毁和管理。Windows在线程之间进行上下文切换,以及在发生垃圾回收的时候,也会浪费很多时间。然而,根据上述讨论,咱们还得出一个结论,那就是有时候必须使用线程,由于它们使Windows变得更健壮,反应更灵敏。

  应该指出的是,安装了多个CPU或者一个多核CPU)的计算机能够真正同时运行几个线程,这提高了应用程序的可伸缩性(在少许的时间里作更多工做的能力)。Windows为每一个CPU内核都分配一个线程,每一个内核都本身执行到其余线程的上下文切换。Windows确保单个线程不会在多个内核上同时被调度,由于这会代理巨大的混乱。今天,许多计算机都包含了多个CPu,超线程CPU或者多核CPU。可是,windows最初设计时,单CPU计算机才是主流,因此Windows设计了线程来加强系统的响应能力和可靠性。今天,线程还被用于加强应用程序的可伸缩性,但在只有多CPU(或多核CPU)计算机上才有可能发生。

TIP:一个时间片结束时,若是Windows决定再次调度同一个线程(而不是切换到另外给一个线程),那么Windows不会执行上下文切换。线程将继续执行,这显著改进了性能。设计本身的代码时注意,上下文切换能避免的就要尽可能避免

2.3 CPU的发展

  过去,CPU速度一直随着时间在变快。因此,在一台旧机器上运行得慢的程序在新机器上通常会快些。然而,CPU 厂商没有延续CPU愈来愈快的趋势。因为CPU厂商不能作到一直提高CPU的速度,因此它们侧重于将晶体管作得愈来愈小,使一个芯片上可以容纳更多的晶体管。今天,一个硅芯片能够容纳2个或者更多的CPU内核。这样一来,若是在写软件时能利用多个内核,软件就能运行得更快些。

  今天的计算机使用了如下三种多CPU技术。

  • 多个CPU
  • 超线程芯片
  • 多核芯片

2.4 使用线程的理由

  使用线程有如下三方面的理由。

  • 使用线程能够将代码同其余代码隔离

  这将提升应用程序的可靠性。事实上,这正是Windows在操做系统中引入线程概念的缘由。Windows之因此须要线程来得到可靠性,是由于你的应用程序对于操做系统来讲是的第三方组件,而微软不会在你发布应用程序以前对这些代码进行验证。若是你的应用程序支持加载由其它厂商生成的组件,那么应用程序对健壮性的要求就会很高,使用线程将有助于知足这个需求。

  • 可使用线程来简化编码

  有的时候,若是经过一个任务本身的线程来执行该任务,或者说单独一个线程来处里该任务,编码会变得更简单。可是,若是这样作,确定要使用额外的资源,也不是十分“经济”(没有使用尽可能少的代码达到目的)。如今,即便要付出一些资源做为代价,我也宁愿选择简单的编码过程。不然,干脆坚持一直用机器语言写程序好了,彻底不必成为一名C#开发人员。但有的时候,一些人在使用线程时,以为本身选择了一种更容易的编码方式,但实际上,它们是将事情(和它们的代码)大大复杂化了。一般,在你引入线程时,引入的是要相互协做的代码,它们可能要求线程同步构造知道另外一个线程在何时终止。一旦开始涉及协做,就要使用更多的资源,同时会使代码变得更复杂。因此,在开始使用线程以前,务必肯定线程真的可以帮助你。

  • 可使用线程来实现并发执行
      若是(并且只有)知道本身的应用程序要在多CPU机器上运行,那么让多个任务同时运行,就能提升性能。如今安装了多个CPU(或者一个多核CPU)的机器至关广泛,因此设计应用程序来使用多个内核是有意义的。

3 数据并行(Data Parallelism)

3.1 数据并行

  数据并行是指对源集合或数组中的元素同时(即并行)执行相同操做的状况。在数据并行操做中,源集合被分区,以便多个线程能够同时在不一样的段上操做。

  数据并行性是指对源集合或数组中的元素同时任务并行库(TPL)经过system.threading.tasks.parallel类支持数据并行。这个类提供了for和for each循环的基于方法的并行实现。

  您为parallel.for或parallel.foreach循环编写循环逻辑,就像编写顺序循环同样。您没必要建立线程或将工做项排队。在基本循环中,您没必要使用锁。底层工做TPL已经帮你处理。

  下面代码展现顺序和并行:

// Sequential version            
foreach (var item in sourceCollection)
{
    Process(item);
}

// Parallel equivalent
Parallel.ForEach(sourceCollection, item => Process(item));

  并行循环运行时,TPL对数据源进行分区,以便循环能够同时在多个部分上运行。在后台,任务调度程序根据系统资源和工做负载对任务进行分区。若是工做负载变得不平衡,调度程序会在多个线程和处理器之间从新分配工做。

  下面的代码来展现如何经过Visual Studio调试代码:

public static void test()
        {
            int[] nums = Enumerable.Range(0, 1000000).ToArray();
            long total = 0;
            
            // Use type parameter to make subtotal a long, not an int
            Parallel.For<long>(0, nums.Length, () => 0, (j, loop, subtotal) =>
            {
                subtotal += nums[j];
                return subtotal;
            },
                (x) => Interlocked.Add(ref total, x)
            );

            Console.WriteLine("The total is {0:N0}", total);
            Console.WriteLine("Press any key to exit");
            Console.ReadKey();
        }
  • 选择调试 > 开始调试,或按F5。
    应用在调试模式下启动,并会在断点处暂停。

  • 在中断模式下打开线程经过选择窗口调试 > Windows > 线程。 您必须位于一个调试会话以打开或请参阅线程和其余调试窗口。

使用场景

3.2 Parallel.For剖析

  查看Parallel.For的底层,

public static ParallelLoopResult For<TLocal>(int fromInclusive, int toExclusive, Func<TLocal> localInit, Func<int, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally);

  清楚的看到有个func函数,看起来很熟悉。

[TypeForwardedFrom("System.Core, Version=3.5.0.0, Culture=Neutral, PublicKeyToken=b77a5c561934e089")]
    public delegate TResult Func<out TResult>();

原来是定义的委托,有多个重载,具体查看文档[https://docs.microsoft.com/en-us/dotnet/api/system.func-4?view=netframework-4.7.2]

  实际上TPL以前,实现并发或多线程,基本都要使用委托。

TIP:关于委托,你们能够查看(https://docs.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/delegates)。或者《细说委托》(https://www.cnblogs.com/laoyu/archive/2013/01/13/2859000.html)

参考

使用场景

相关文章
相关标签/搜索