.NET的并发编程(TPL编程)是什么? (下)

简介: .NET的并发编程(TPL编程)是什么? (下)

2.3 CPU的发展

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

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

  1. 多个CPU
  2. 超线程芯片
  3. 多核芯片

2.4 使用线程的理由

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

  1. 使用线程可以将代码同其他代码隔离
          这将提高应用程序的可靠性。事实上,这正是Windows在操作系统中引入线程概念的原因。Windows之所以需要线程来获得可靠性,是因为你的应用程序对于操作系统来说是的第三方组件,而微软不会在你发布应用程序之前对这些代码进行验证。如果你的应用程序支持加载由其它厂商生成的组件,那么应用程序对健壮性的要求就会很高,使用线程将有助于满足这个需求。
  2. 可以使用线程来简化编码
          有的时候,如果通过一个任务自己的线程来执行该任务,或者说单独一个线程来处里该任务,编码会变得更简单。但是,如果这样做,肯定要使用额外的资源,也不是十分“经济”(没有使用尽量少的代码达到目的)。现在,即使要付出一些资源作为代价,我也宁愿选择简单的编码过程。否则,干脆坚持一直用机器语言写程序好了,完全没必要成为一名C#开发人员。但有的时候,一些人在使用线程时,觉得自己选择了一种更容易的编码方式,但实际上,它们是将事情(和它们的代码)大大复杂化了。通常,在你引入线程时,引入的是要相互协作的代码,它们可能要求线程同步构造知道另一个线程在什么时候终止。一旦开始涉及协作,就要使用更多的资源,同时会使代码变得更复杂。所以,在开始使用线程之前,务必确定线程真的能够帮助你。
  3. 可以使用线程来实现并发执行
          如果(而且只有)知道自己的应用程序要在多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)

回到顶部

4 数据和任务并行中潜在的缺陷

      在许多情况下,parallel.for和parallel.foreach可以比普通的顺序循环提供显著的性能改进。然而,并行循环的工作引入了复杂性,这可能会导致在顺序代码中不常见或根本不会遇到的问题。本主题列举了一些实践来帮您避免这些问题,当你在写并行代码的时候。

4.1 不要假设并行总是很快

     在某些情况下,并行循环的运行速度可能比其顺序等效循环慢。基本的经验法则是,具有很少迭代和快速用户委托的并行循环不太可能加快速度。但是,由于有很多因素会影响性能,我建议您测量实际结果。

4.2 避免写入共享缓存

     在顺序代码中,读写静态变量或者字段是很正常的。然而,每当多个线程同时访问这些变量时,就有很大的竞争条件潜力。即使您可以使用锁来同步对变量的访问,同步成本也会损害性能。因此,我们建议您尽可能避免或至少限制对并行循环中共享状态的访问。最好的方式是使用Parallel.For 和 Parallel.ForEach的重载方法,在并行循环期间,它们使用System.Threading.ThreadLocal泛型类型的变量来存储线程本地状态。通过使用并行循环,您将产生划分源集合和同步工作线程的开销。并行化的好处进一步受到计算机上处理器数量的限制。在一个处理器上运行多个计算绑定线程并不能加快速度。因此,要注意不要过度使用并行。

过度使用并行最常见的场景发生在嵌套循环中。在大多数情况下,最好仅在外层循环使用并行,除非以下几种场景适用:

  • 内层循环很长
  • 您正在对每笔订单执行昂贵的计算。
  • 目标系统有足够的处理器来处理通过并行处理对客户订单的查询而产生的线程数。

在所有情况下,确定最佳查询形状的最佳方法都是测试和度量。

4.3 避免调用非线程安全的方法

      从并行循环中写入非线程安全的实例方法可能会导致数据损坏,这在程序中可能会被检测到,也可能不会被检测到。它可能导致异常。在以下示例中,多线程会尝试同时调用FileStream.WriteByte方法,但是这个是不被支持的。

FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => 

参考文献:

  1. https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/
  2. https://docs.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/delegates
  3. https://www.cnblogs.com/laoyu/archive/2013/01/13/2859000.html
  4. 《C#并发经典实例》
  5. 《CLR via C#》第3版
  6. https://www.52interview.com/solutions/38
相关文章
|
5月前
|
存储 JSON 开发工具
Visual Studio编程效率提升技巧集(提高.NET编程效率)
Visual Studio编程效率提升技巧集(提高.NET编程效率)
Visual Studio编程效率提升技巧集(提高.NET编程效率)
|
2月前
|
传感器 数据采集 物联网
探索.NET nanoFramework:为嵌入式设备编程的新途径
探索.NET nanoFramework:为嵌入式设备编程的新途
51 7
|
4月前
|
大数据 开发工具 开发者
从零到英雄:.NET核心技术带你踏上编程之旅,构建首个应用,开启你的数字世界探险!
【8月更文挑战第28天】本文带领读者从零开始,使用强大的.NET平台搭建首个控制台应用。无论你是新手还是希望扩展技能的开发者,都能通过本文逐步掌握.NET的核心技术。从环境搭建到创建项目,再到编写和运行代码,详细步骤助你轻松上手。通过计算两数之和的小项目,你不仅能快速入门,还能为未来开发更复杂的应用奠定基础。希望本文为你的.NET学习之旅开启新篇章!
35 1
|
4月前
|
存储 C#
揭秘C#.Net编程秘宝:结构体类型Struct,让你的数据结构秒变高效战斗机,编程界的新星就是你!
【8月更文挑战第4天】在C#编程中,结构体(`struct`)是一种整合多种数据类型的复合数据类型。与类不同,结构体是值类型,意味着数据被直接复制而非引用。这使其适合表示小型、固定的数据结构如点坐标。结构体默认私有成员且不可变,除非明确指定。通过`struct`关键字定义,可以包含字段、构造函数及方法。例如,定义一个表示二维点的结构体,并实现计算距离原点的方法。使用时如同普通类型,可通过实例化并调用其成员。设计时推荐保持结构体不可变以避免副作用,并注意装箱拆箱可能导致的性能影响。掌握结构体有助于构建高效的应用程序。
131 7
|
4月前
|
Java Spring 自然语言处理
Spring 框架里竟藏着神秘魔法?国际化与本地化的奇妙之旅等你来揭开谜底!
【8月更文挑战第31天】在软件开发中,国际化(I18N)与本地化(L10N)对于满足不同地区用户需求至关重要。Spring框架提供了强大支持,利用资源文件和`MessageSource`实现多语言文本管理。通过配置日期格式和货币符号,进一步完善本地化功能。合理应用这些特性,可显著提升应用的多地区适应性和用户体验。
43 0
|
4月前
|
开发者
在.NET 中进行并发编程,究竟隐藏着哪些让开发者头疼不已的挑战?又该如何破解?
【8月更文挑战第28天】在现代软件开发中,并发编程的重要性日益凸显,但.NET开发者们却常常遇到资源竞争与死锁等挑战。例如,多线程对共享资源的访问可能导致数据不一致。以上提供了一个因缺乏同步机制而导致计数器结果出错的例子,并通过使用锁解决了该问题。此外,还介绍了一个产生死锁的代码片段,展示两个线程因互相等待对方持有的锁而陷入僵局。通过对这些挑战的理解和应对,可以提高软件的稳定性和效率。
29 0
|
4月前
|
传感器 数据采集 物联网
探索未来:.NET nanoFramework引领嵌入式设备编程革新之旅
【8月更文挑战第28天】.NET nanoFramework 是一款专为资源受限的嵌入式设备设计的轻量级、高性能框架,基于 .NET Core,采用 C# 进行开发,简化了传统底层硬件操作的复杂性,极大提升了开发效率。开发者可通过 Visual Studio 或 Visual Studio Code 快速搭建环境并创建项目,利用丰富的库和驱动程序轻松实现从基础 LED 控制到网络通信等多种功能,显著降低了嵌入式开发的门槛。
84 0
|
6月前
|
SQL 开发框架 安全
【.NET Core】深入理解任务并行库 (TPL)
【.NET Core】深入理解任务并行库 (TPL)
73 0
|
7月前
|
JSON 编解码 Go
Golang深入浅出之-HTTP客户端编程:使用net/http包发起请求
【4月更文挑战第25天】Go语言`net/http`包提供HTTP客户端和服务器功能,简化高性能网络应用开发。本文探讨如何发起HTTP请求,常见问题及解决策略。示例展示GET和POST请求的实现。注意响应体关闭、错误处理、内容类型设置、超时管理和并发控制。最佳实践包括重用`http.Client`,使用`context.Context`,处理JSON以及记录错误日志。通过实践这些技巧,提升HTTP编程技能。
85 1
|
7月前
|
Go 开发者
Golang深入浅出之-HTTP客户端编程:使用net/http包发起请求
【4月更文挑战第24天】Go语言的`net/http`包在HTTP客户端编程中扮演重要角色,但使用时需注意几个常见问题:1) 检查HTTP状态码以确保请求成功;2) 记得关闭响应体以防止资源泄漏;3) 设置超时限制,避免长时间等待;4) 根据需求处理重定向。理解这些细节能提升HTTP客户端编程的效率和质量。
82 1

热门文章

最新文章