3

我一直在玩弄 .NET 4.0 中的 Parallel 库。最近,我为我们的大型系统之一必须使用的一些不寻常的读/写操作开发了一个自定义 ORM。这允许我用属性装饰一个对象,并通过反射确定它必须从数据库中提取哪些列,以及它必须在写入时输出哪些 XML。

因为我设想这个包装器可以在许多项目中重用,所以我想尽可能地加快速度。该库主要用于 .NET Web 应用程序。我正在使用一次性控制台应用程序测试框架,以查看我创建的类。

我现在已经吸取了多线程带来的开销的教训。多线程会导致它运行得更慢。从周围阅读,这对于已经做了很长时间的人来说似乎很直观,但对我来说实际上是违反直觉的:如何同时运行一个方法 30次比顺序运行它 30 次要慢?

我认为我不会因为多个线程不得不争夺同一个共享对象而引起问题(尽管我还不够擅长它还不能确定),所以我认为减速来自开销产生所有这些线程和运行时使它们保持正常。所以:

  • 虽然我主要是作为一个学习练习来做的,但这是悲观吗?对于琐碎的非 IO 任务,多线程是否过大?我的主要目标是速度,而不是 UI 或任何东西的响应能力。
  • 由于线程池中已经创建了线程,在 IIS 中运行相同的多线程代码会导致它加速,而现在我正在使用控制台应用程序,我假设它是单线程的,直到我另有说明?我即将进行一些测试,但我认为我缺少一些基础知识来了解为什么它会是一种或另一种方式。我的控制台应用程序也在我的桌面上运行,具有两个内核,而用于 Web 应用程序的服务器会有更多内核,因此我可能也必须将其用作变量。
4

3 回答 3

8

线程实际上并非全部同时运行。

在台式机上,我假设您有一个双核 CPU(最多可能是四核)。这意味着只能同时运行 2/4 个线程。

如果您产生了 30 个线程,操作系统将不得不在这 30 个线程之间进行上下文切换以保持它们全部运行。上下文切换非常昂贵,因此速度放缓。

作为一个基本建议,如果您尝试优化计算,我的目标是每个 CPU 1 个线程。除此之外,您并没有真正做任何额外的工作,您只是在同一个 CPU 上交换线程。试着把你的电脑想象成里面有有限数量的工人,你不能同时做比你可用的工人数量更多的工作。

.net 4.0 并行任务库中的一些新功能允许您执行考虑线程数量可伸缩性的操作。例如,您可以创建一堆任务,任务并行库将在内部计算出您有多少可用 CPU,并优化创建/使用的线程数以免 CPU 过载,因此您可以创建 30 个任务,但在双核机器上,TP 库仍然只会创建 2 个线程,并将 . 显然,当您在更大的机器上运行它时,它会很好地扩展。或者您可以使用诸如ThreadPool.QueueUserWorkItem(...)将一堆任务排队的东西,并且池将自动管理用于执行这些任务的线程数。

是的,线程创建有很多开销,但是如果您使用 .net 线程池(或 4.0 中的并行任务库),.net 将管理您的线程创建,您实际上可能会发现它创建的线程少于您创建的任务数。它将在可用线程上内部交换您的任务。如果您确实想控制实际线程的显式创建,则需要使用 Thread 类。

[一些 cpu 可以用线程做一些聪明的事情,并且每个 CPU 可以运行多个线程 - 请参阅超线程- 但请查看您的任务管理器,如果您在今天的桌面上拥有超过 4-8 个虚拟 CPU,我会感到非常惊讶]

于 2010-01-12T16:12:04.263 回答
2

这有很多问题,因此了解幕后发生的事情是值得的。我强烈推荐 Joe Duffy 的“Windows 上的并发编程”一书和“Java 并发实践”一书。后者在编写多线程代码时需要了解处理器架构的级别。您将遇到的一个会损害您的代码的问题是缓存,或者更有可能是缺少缓存。

如前所述,调度和运行线程会产生开销,但您可能会发现跨线程共享数据时会产生更大的开销。该数据可能会从处理器缓存刷新到主内存中,这将导致您的代码严重减速。

这是托管环境应该保护我们免受的那种低级别的东西,但是,当编写高度并行的代码时,这正是你必须处理的那种问题。

我的一位同事录制了有关 Parallel.For 和 Parallel.ForEach 的性能问题的截屏视频,这可能会有所帮助:

http://rocksolidknowledge.com/ScreenCasts.mvc/Watch?video=ParallelLoops.wmv

于 2010-01-12T16:27:15.417 回答
1

你说的是 ORM,所以我认为一定数量的 I/O 正在进行。如果是这种情况,线程创建和上下文切换的开销将相对不存在。

最有可能的是,您遇到了 I/O 争用:如果您以乱序方式读取同一组数据,则读取速度可能比按顺序读取要慢(尤其是在旋转硬盘驱动器上,但在其他存储设备上也是如此)。 -命令。因此,如果您正在执行 30 个数据库查询,如果它们都由同一个 I/O 设备支持并且查询不在缓存中,那么它们可能会比并行运行更快。并行运行它们可能会导致系统几乎同时有一堆 I/O 读取请求,这可能会导致操作系统依次读取每个请求的一小部分 - 导致您的驱动器头来回跳跃,浪费宝贵的毫秒。

但这只是一个猜测;在不了解更多信息的情况下,不可能真正确定是什么导致了您的减速。

尽管与添加两个数字相比,创建线程“非常昂贵”,但通常不会轻易过度。如果您的操作非常短(例如,一毫秒或更短),使用线程池而不是新线程将显着节省时间。一般来说,如果你的操作很短,你应该重新考虑并行的粒度;也许您最好将计算拆分成更大的块:例如,通过拥有相当少的工作任务来一次处理整批较小的工作项,而不是单独处理每个项目。

于 2010-01-12T16:25:08.897 回答