为啥不使用大量的多线程代码?

Posted

技术标签:

【中文标题】为啥不使用大量的多线程代码?【英文标题】:Why not to use massively multi-threaded code?为什么不使用大量的多线程代码? 【发布时间】:2015-10-24 00:32:19 【问题描述】:

如今,随着 node.js 的流行、Python 3.5 最近的async 改进等等,异步和其他基于事件的编程范例似乎像野火一样蔓延开来。

并不是我特别介意这个,或者我自己已经很长时间没有这样做了,但我一直在努力寻找真正的原因。一直在寻找同步编程的弊端似乎是为了消除先入为主的观念,即“你不能为每个请求都有一个线程”,而没有真正限定该陈述。

为什么不呢?线程可能不是人们能想到的最便宜的资源,但它看起来并不“昂贵”。在 64 位机器上,我们有足够多的虚拟地址空间来处理我们可能想要的所有线程,并且,除非您的调用链相当深,否则每个线程不一定需要比单个页面更多的物理 RAM * 用于堆栈加上内核和 libc 需要的任何小开销。至于性能,我自己的随意测试表明,Linux 在单个 CPU 上每秒可以处理超过 100,000 个线程创建和拆除,这几乎不是瓶颈。

话虽如此,我并不认为基于事件的编程只是一个诡计,因为它似乎是允许诸如 lighttpd/nginx/whatever 之类的 HTTP 服务器在高并发性能方面超越 Apache 的主要驱动力**。但是,我一直在尝试找到某种实际的调查,以了解 为什么 大规模多线程程序速度较慢但找不到任何原因的原因。

那么,这是为什么呢?


*我的测试似乎表明每个线程实际上需要两个页面。也许 TLS 有一些脏东西或其他什么,但它似乎并没有太大变化。

**虽然也应该说,当时的 Apache 使用的是基于进程的并发,而不是基于线程的,这显然有很大的不同。

【问题讨论】:

***.com/questions/9964899/…的可能重复 1) 内存限制,例如在 Windows 上,每个线程需要 1MB 的堆栈,你真的想在页面文件上敲打只是为了运行每个线程......和 ​​2) 上下文的开销切换可能会限制您的整体应用程序性能,但只有您可以确定。 @ChrisO:诚然,我不了解 Windows,但我所知道的所有 POSIX 系统都只为堆栈分配虚拟地址空间,并懒惰地分配物理页面,正如我在问题中描述的那样。至于性能,使用基于事件的编程的主要原因是处理无论如何都会阻塞请求的请求,因此 CPU 瓶颈似乎不是正在解决的问题。 那么为什么每个线程被限制为只有一页内存呢?他们什么都不做吗? @ChrisO:它们并没有“限制”到 1 页堆栈。我只是说大多数东西可能不会使用更多,除非它们的调用链相对较深。 【参考方案1】:

如果每个请求都有一个线程,那么在不切换上下文 100 次的情况下,您无法为 100 个请求中的每一个做一点工作。尽管计算机必须做的许多事情随着时间的推移变得越来越快,但上下文切换仍然很昂贵,因为它会耗尽缓存,而且现代系统比以往任何时候都更加依赖这些缓存。

【讨论】:

现代上下文切换不会破坏缓存。当然,操作系统可能会将其中的一部分用于调度程序等,但任何基于事件的系统也需要部分缓存来进行簿记。您是否有显示上下文切换是问题的来源? @Dolda2000 哦,我明白了问题所在。通过“上下文切换”,我的意思是当你切换线程时会发生什么。发送网络响应时不会切换线程。事实上,一个经常调用网络代码的线程很可能会在每次调用时发现缓存中的网络代码是热的。缓存被炸毁是因为它们保存了另一个线程正在做的事情,而不是这个线程正在做的事情。为了获得良好的性能,您希望让一个线程尽可能长时间地运行,并尽可能与之前的工作相似。 完全不同。当你在线程之间切换时,缓存包含旧线程正在做的所有事情,对你没有好处。当执行大量网络 I/O 的线程调用内核时,网络 I/O 代码在缓存中可能仍然很热。这两种情况完全相反。 我怀疑这种意见分歧可以不用数字来解决。一般来说,多线程上的 FUD 级别对于有意义的讨论来说已经太高了:( @Dolda2000 对于线程不循环通过大量 L1++ 数组并因此导致共享总线抖动的应用程序,我已经有数字表明,对于 CPU 密集型任务,我的 4/8 核 i7大约 32 个线程的性能达到顶峰,而不是通常建议为最佳的 8 个线程。我必须有数字让我相信任何事情。多线程或其他方面的整体性能。 OTOH,如果按顺序访问大型数组,则 perf.只有 16 个线程是非常糟糕的。必须有真实测试的数字!【参考方案2】:

这是一个加载的问题。随着时间的推移,我听到了不同的回应,因为我之前与不同的开发人员进行过多次对话。主要是,我的直觉是大多数开发人员讨厌它,因为编写多线程代码更难,有时很容易不必要地自取其辱。也就是说,每种情况都不同。有些程序非常适合多线程,例如网络服务器。每个线程都可以接受请求并基本上处理它,而无需太多外部资源。它有一套程序可应用于决定如何处理它的请求。它决定如何处理它并将其传递出去。所以它是相当独立的,可以相当安全地在自己的世界中运行。所以这是一个很好的线程。

其他情况可能不太适合。特别是当您需要共享资源时。事情很快就会变得棘手。即使你做了看似完美的上下文切换,你仍然可能会遇到竞争条件。然后噩梦开始了。这在巨大的单体应用程序中很常见,他们选择使用线程并为他们的开发团队打开地狱之门。

最后,我认为我们可能不会在日常开发中看到更多线程,但我们将转向一个更像事件驱动的世界。随着微服务的出现,我们正在沿着这条路线进行 Web 开发。所以可能会使用更多的线程,但不是以使用框架的开发人员可见的方式。它只是框架的一部分。至少这是我的看法。

【讨论】:

'编写多线程代码更难' - 好吧,即使这是有争议的。异步代码需要用户空间状态机,而每连接线程模型使用内核空间状态机。结果 - 每个连接线程的用户代码是直接内联的,而异步代码是多个回调。 我同意这是一个加载的问题,并且我绝对同意某些程序更直接地适用于一个模型或另一个模型。尽管如此,性能问题本身应该可以客观地回答。 哈哈哈,真有趣。 @martin,我想你误解了我所说的多线程代码的意思。我的意思是您必须特别声明互斥体并将其释放到代码的关键部分的代码。我在想 C++ 或 JAVA。人们倾向于过早或过晚释放互斥锁,或者不得不担心竞争条件。如果代码不是多线程的,那么当逻辑相当简单时,还有很多需要考虑的事情。更难,我的意思是增加对死锁和崩溃的恐惧。 我认为一个更好的问题可能是,我们如何设计能够更好地让自己拥有可以彼此异步运行的小型独立组件的系统?对此,响应可能是在大型应用程序上具有某种基于事件的系统或微服务环境。所以在某种程度上,我们正朝着那个方向前进,但我同意,我们还没有到达那里。【参考方案3】:

一旦就绪或正在运行的线程(相对于事件挂起的线程)和/或进程的数量超过内核数量,那么这些线程和/或进程就会竞争相同的内核、相同的缓存和相同的内存巴士。

除非有大量的同时发生的事件要等待,否则我看不到大规模多线程代码的目的,除了具有大量处理器和内核的超级计算机,而且该代码通常是大量多线程的-处理,具有多个内存总线。

【讨论】:

虽然这是真的,但在我看来与这个问题无关。基于事件的编程解决的问题不是 CPU 瓶颈,而是在处理某个请求时处理 I/O 阻塞。此外,即使是基于事件编码的程序通常也会将正在完成的工作分配到至少与系统中 CPU 数量一样大的线程池上,因此无论哪种方式,竞争似乎都是相同的。 不,您的意思是“一旦 READY 或 RUNNING 线程和/或进程的数量竞争相同的内核,而 RUNNING 线程和/或进程的数量正在竞争相同的 L2 或更高级别缓存和内存总线。

以上是关于为啥不使用大量的多线程代码?的主要内容,如果未能解决你的问题,请参考以下文章

python中的多线程为啥会报错?

为啥我的多线程代码没有更快?

知乎为啥使用Tornado?使用Python中的多线程特性了吗

为啥 C++ 中的多线程会降低性能

实现Runnable的多线程代码中,while(true)表示的啥含义?为啥没有while(t

知乎为啥使用Tornado?使用Python中的多线程特性了吗