《 .NET平台上的高性能代码》一书。 第二版

图片 本书将教您如何最大程度地提高托管代码的性能,理想情况下不牺牲.NET环境的任何好处,或者在最坏的情况下,牺牲其中的最少数目。 您将学习合理的编程方法,找出需要避免的地方,最重要的是,最重要的是,如何使用免费提供的工具以轻松地衡量生产率水平。 培训材料中的水最少-只有最需要的。 这本书给出了您需要知道的确切内容,它既相关又简洁,没有太多内容。 大多数章节以常规信息和背景开头,其后是特定技巧,像配方一样列出,最后以针对各种场景的逐步测量和调试部分结尾。

在此过程中,Ben Watson将沉浸于.NET环境的特定组件中,尤其是其基础的公共语言运行时(CLR),并查看如何管理您的计算机内存,如何生成代码,组织多线程执行以及完成更多工作。 。 您将看到.NET体系结构如何同时限制您的软件工具并为其提供附加功能,以及编程路径的选择如何显着影响应用程序的整体性能。 作为奖励,作者将与您分享过去九年来在Microsoft创建大型,复杂,高性能.NET系统的经验。

摘录:选择适当的线程池大小


随着时间的流逝,线程池是独立配置的,但是从一开始它就没有历史记录,它将以初始状态启动。 如果您的软件产品异常异步并且大量使用中央处理器,则可能会遭受过高的初始启动成本,从而无法创建甚至更多线程。 调整启动参数将有助于更快地达到稳定状态,因此从应用程序启动的那一刻起,您就可以使用一定数量的现成线程:

const int MinWorkerThreads = 25; const int MinIoThreads = 25; ThreadPool.SetMinThreads(MinWorkerThreads, MinIoThreads); 

小心点 使用Task对象时,其分配将基于为此可用的线程数。 如果它们太多,则Task对象可能会进行过多的调度,这至少会导致由于频繁切换上下文而导致中央处理器的效率下降。 如果工作量不是很高,则线程池可以切换到使用可以减少线程数的算法,从而将其数量减少到指定数量以下。

您也可以使用SetMaxThreads方法设置它们的最大数量,但是这种技术也面临类似的风险。

要查找所需的线程数,请保留此参数,并使用ThreadPool.GetMaxThreads和ThreadPool.GetMinThreads方法或显示该过程涉及的线程数的性能计数器来分析处于稳定状态的应用程序。

不要中断流程


在不与其他线程的工作协调的情况下中断线程的工作是一个相当危险的过程。 流必须清理自己,并且将它们称为Abort方法不允许它们关闭而不会带来负面影响。 销毁线程后,应用程序的某些部分处于未定义状态。 最好使程序崩溃,但理想情况下需要重新启动。

为了安全地终止线程,您需要使用某种共享状态,并且线程函数本身必须检查该状态以确定何时应完成。 必须通过一致性来实现安全性。

通常,您应该始终使用Task对象-未提供API来中断任务。 为了能够一致地终止线程,您必须如前所述,使用CancellationToken令牌。

不更改线程优先级


通常,更改线程的优先级是极其失败的。 在Windows上,根据线程的优先级执行线程调度。 如果高优先级的线程总是准备好运行,那么低优先级的线程将被忽略并且很少有机会运行。 通过增加线程的优先级,您可以说线程的工作应优先于所有其他工作,包括其他进程。 这对于稳定的系统而言并不安全。

如果线程正在运行可以等待正常优先级的任务完成的线程,则最好降低线程的优先级。 降低线程优先级的一个好理由可能是发现执行无限循环的失控线程。 安全中断线程是不可能的,因此返回给定线程和处理器资源的唯一方法是重新启动进程。 在有可能关闭流并进行干净处理之前,降低失控流的优先级将是使后果最小化的合理方法。 应该注意的是,即使是具有较低优先级的线程也仍然可以保证随着时间的推移运行:被剥夺启动时间的时间越长,Windows将为其设置的动态优先级就越高。 空闲优先级THREAD_- PRIORITY_IDLE是一个例外,其中操作系统仅安排线程在其实际上没有其他要启动的情况下执行。

可能有充分的理由来增加流程的优先级,例如,需要快速响应罕见的情况。 但是使用这种技术应该非常谨慎。 Windows中的线程调度与它们所属于的进程无关,因此将启动进程中的高优先级线程,这不仅会损害其他线程,还会损害系统上运行的其他应用程序的所有线程。

如果使用了线程池,则每次线程返回到池时,所有优先级更改都将被丢弃。 如果在使用“任务并行”库时继续管理基本线程,则应记住,可以在同一线程中启动多个任务,然后再将其返回到池中。

线程同步和阻塞


一旦对话进入多个线程,就必须同步它们。 同步在于仅提供一个线程访问共享状态(例如,访问类字段)。 通常,线程使用同步对象(例如Monitor,Semaphore,ManualResetEvent等)进行同步。有时,它们被非正式地称为锁,而特定线程中的同步过程称为锁。

关于锁的基本真理之一是:它们永远不会提高性能。 在最佳情况下-同步原语实现良好且没有竞争-阻塞可以是中立的。 这会导致其他线程停止执行有用的工作,并浪费CPU时间,增加上下文切换时间并导致其他负面后果。 您必须忍受这一点,因为正确性比简单的性能重要得多。 是否快速计算出不正确的结果并不重要!

在开始解决使用锁具的问题之前,我们将考虑最基本的原理。

我是否需要关心性能?


有理由首先需要提高生产率。 这使我们回到了第1章中讨论的原理。对于您的所有应用程序代码,性能并不是同等重要。 并非所有代码都必须经过n级优化。 通常,这一切都始于“内部循环”(最常执行或对性能最关键的代码),并向各个方向扩散,直到成本超过获得的收益为止。 就性能而言,代码中有许多方面的重要性要小得多。 在这种情况下,如果您需要锁,请冷静地应用它。

现在,您应该小心。 如果您的非关键代码段是在线程池中的线程中执行的,并且阻塞了很长时间,则线程池可能会开始插入更多线程来处理其他请求。 如果不时有一个或两个线程这样做,那就可以了。 但是,如果许多线程执行此类操作,则可能会出现问题,因为这样,必须执行实际工作的资源会被浪费掉。 由于不必要的上下文切换或线程池的不合理参与,即使在程序中以相当大的恒定负载启动程序时,疏忽大意也会对系统造成负面影响,即使那些高性能不重要的部分也是如此。 与所有其他情况一样,必须进行测量以评估情况。

您真的需要锁吗?


最有效的锁定机制是无效的。 如果可以完全消除对线程同步的需求,那么这将是获得高性能的最佳方法。 这是一个不太容易实现的理想。 通常,这意味着您需要确保没有可变的共享状态-通过应用程序的每个请求都可以独立于另一个请求或某些集中的可变(读写)数据进行处理。 此功能将是实现高性能的最佳方案。

并且仍然要小心。 通过重组,可以轻松地将其覆盖范围过大并将代码变成混乱的混乱局面,包括您自己在内的任何人都无法发现。 除非高生产率确实是一个关键因素,否则您不应走得太远。 将代码转换为异步和独立代码,但仍保持清晰。

如果仅从一个变量读取多个线程(并且没有从流中写入该变量的提示),则不需要同步。 所有线程都可以无限制地访问。 这自动适用于不可变对象,例如字符串或不可变类型的值,但如果您保证在多个线程读取期间其值的不可变性,则可以应用于任何类型的对象。

如果有多个线程写入某个共享变量,请查看是否可以通过使用局部变量来消除同步访问。 如果您可以创建工作的临时副本,则同步需求将消失。 这对于重复同步访问尤为重要。 从重新访问共享变量,您需要转到对共享变量的一次性访问之后重新访问局部变量,如以下简单的示例所示,将项目添加到由多个线程集合共享的共享中。

 object syncObj = new object(); var masterList = new List<long >(); const int NumTasks = 8; Task[] tasks = new Task[NumTasks]; for (int i = 0; i < NumTasks; i++) { tasks[i] = Task.Run(()=> { for (int j = 0; j < 5000000; j++) { lock (syncObj) { masterList.Add(j); } } }); } Task.WaitAll(tasks); 

此代码可以按如下方式转换:

 object syncObj = new object(); var masterList = new List<long >(); const int NumTasks = 8; Task[] tasks = new Task[NumTasks]; for (int i = 0; i < NumTasks; i++) { tasks[i] = Task.Run(()=> { var localList = new List<long >(); for (int j = 0; j < 5000000; j++) { localList.Add(j); } lock (syncObj) { masterList.AddRange(localList); } }); } Task.WaitAll(tasks); 

在我的机器上,第二个版本的代码运行速度是第一个版本的两倍。
最终,可变的共享状态是性能的根本敌人。 它需要同步以确保数据安全,从而降低性能。 如果您的设计至少有丝毫机会避免阻塞,那么您将接近实现理想的多线程系统。

同步偏好顺序


在确定是否需要任何类型的同步时,应理解并非所有同步都具有相同的性能或行为特征。 在大多数情况下,您只需要使用锁,通常这应该是原始选项。 为了证明额外的复杂性,使用除阻塞之外的其他方法需要进行大量测量。 通常,我们按以下顺序考虑同步机制。

1.锁/类监视器-保持代码的简单性,易懂性,并提供良好的性能平衡

2.完全缺乏同步。 摆脱共享的可变状态,进行重组和优化。 这比较困难,但是,如果成功,它将比使用阻塞更好地工作(除非出错或体系结构降级)。

3.用于联锁的简单联锁方法-在某些情况下,它们可能更合适,但是一旦情况变得更加复杂,请立即使用锁锁。

最后,如果您真的可以证明使用它们的好处,请使用更复​​杂,更复杂的锁(请记住:它们很少像您期望的那样有用):

  1. 异步锁(将在本章后面讨论);
  2. 其他人

特定情况可能会指示或阻碍某些此类技术的使用。 例如,组合多个互锁方法不太可能胜过单个锁语句。

»这本书的更多信息可以在出版商的网站上找到
» 目录
» 摘录

小贩优惠券可享受25%的折扣-.NET

支付纸质版本的书后,就会通过电子邮件发送电子书。

Source: https://habr.com/ru/post/zh-CN458520/


All Articles