什么时候多线程不是一个好主意?
我最近正在开发一个通过以太网和串行发送和接收消息的应用程序。然后,我受命添加对DIO离散量的监视。我说
"No reason to interrupt the main thread which is involved in message processing, I'll just create another thread that monitors DIO."
然而,这一决定被证明是糟糕的。有时主线程会在发送和接收串行消息之间中断。这种中断会打乱时间,而且消息会丢失(永远)。
我发现了另一种无需使用其他线程即可监视DIO的方法,并且以太网和串行通信已恢复为正确的功能。
然而,整个惨败让我思考。他们是否有关于何时不使用多线程的一般指导原则,和/或者没有人再有使用多线程不是一个好主意的例子吗?
**编辑:基于评论,并且在浪费互联网的信息之后,我撰写了一篇博客文章,标题为"多线程何时不是一个好主意?
解决方案
实际上,多线程是不可扩展的,并且很难调试,因此在任何情况下都应避免使用它。在少数情况下,强制执行是必不可少的:当多CPU上的性能很重要时,或者当我们处理的服务器上有很多客户端需要很长时间才能回答时。
在任何其他情况下,我们都可以使用诸如队列+ cron作业之类的替代方法。
原则上,每次调用者在队列中等待时都没有开销。
流程是并行的吗?性能真的值得关注吗?是否像Web服务器上一样有多个执行"线程"?我不认为答案是有限的。
如果我们需要保证精确的物理时序(例如示例),则多线程不是一个好主意。其他缺点包括线程之间的密集数据交换。我想说的是,如果我们不太在意它们的相对速度/优先级/定时,那么多线程对于真正的并行任务很有用。
- 在单处理器计算机和桌面应用程序上,我们使用多线程,因此我们不会冻结该应用程序,但实际上没有其他用途。
- 在单处理器服务器和基于Web的应用程序上,不需要多线程,因为Web服务器可以处理大多数线程。
- 建议在多处理器计算机和桌面应用程序上使用多线程和并行编程。使线程与处理器数量一样多。
- 在多处理器服务器和基于Web的应用程序上,无需再次使用多线程,因为Web服务器可以处理它。
总体而言,如果我们将多个线程用于非冻结桌面应用程序和任何其他通用答案,而不是将其冻结,则如果我们拥有一台核心计算机,则由于线程彼此中断,会使应用程序变慢。
为什么?因为有硬件开关。硬件总共要花时间在线程之间进行切换。在多核设备上,继续并为每个核使用1个线程,我们将大大看到它的增加。
改写一句老话:程序员遇到了问题。他想:"我知道,我会使用线程。"现在程序员有两个问题。 (通常归因于JWZ,但似乎早于他使用正则表达式时使用它。)
一个好的经验法则是"不要使用线程,除非有非常令人信服的理由使用线程。"多个线程正在自找麻烦。尝试找到一种无需使用多个线程即可解决该问题的好方法,并且只有避免使用它与使用线程的额外努力一样多的麻烦时,才退回到使用线程的方式。另外,如果我们正在多核/多CPU机器上运行,请考虑切换到多个线程,并且单线程版本的性能测试表明我们需要额外的内核的性能。
我写的一个最近的应用程序必须使用多线程(尽管线程数不是无限的),我必须通过两个协议在多个方向上进行通信,还要监视第三个资源的更改。这两个协议库都需要一个线程来运行相应的事件循环,考虑到这些事件后,很容易为资源监视创建第三个循环。除了事件循环要求之外,通过电线传递的消息还具有严格的时序要求,一个循环不会冒阻塞另一个循环的风险,而使用多核CPU(SPARC)可以进一步缓解这种情况。
关于是否应该将每个消息处理视为线程池中分配给线程的工作,还有进一步的讨论,但是最后这是一个扩展,不值得该工作。
如果可能,仅在可以将工作划分为定义良好的工作(或者一系列工作)时才考虑所有线程,以便语义相对易于记录和实现,并且可以对线程设置上限。我们使用且需要交互的线程数。最好应用此功能的系统几乎是消息传递系统。
我会说多线程通常用于:
- 当GUI保持响应时,允许在后台进行数据处理
- 将非常大的数据分析拆分为多个处理单元,以便更快地获得结果。
- 当我们从某些硬件接收数据并需要一些东西来将其连续添加到缓冲区时,而其他元素则决定如何处理它(写入磁盘,在GUI上显示等)。
因此,如果我们不解决这些问题之一,则添加线程不太可能使生活更轻松。实际上,几乎可以肯定,它将变得更加困难,因为正如其他人所提到的;与单线程解决方案相比,调试多线程应用程序的工作量要大得多。
安全性可能是避免使用多个线程(在多个进程上)的原因。有关多进程安全功能的示例,请参见Google chrome。
我们可能想看一下Dan Kegel的" C10K问题"网页,该网页处理多个数据源/接收器。
基本上,最好使用最少的线程,该线程可以在大多数操作系统的某些事件系统中(或者在Windows中使用IOCP异步执行)在套接字中完成。
当我们遇到OS和/或者库不提供非阻塞方式进行通信的情况时,最好在报告回到同一事件循环时使用线程池来处理它们。
布局示例图:
Per CPU [*] EVENTLOOP ------ Handles nonblocking I/O using OS/library utilities | \___ Threadpool for various blocking events Threadpool for handling the I/O messages that would take long
还有两个使用线程的可能原因:
- 平台缺少异步I / O操作,例如Windows ME(没有完成端口或者I / O重叠,在移植使用它们的XP应用程序时会很麻烦。)Java 1.3和更早版本。
- 可以挂起的第三方库函数,例如如果远程服务器已关闭,并且该库无法提供取消操作的方式,则无法对其进行修改。
在密集处理期间保持GUI响应并不总是需要其他线程。一个回调函数通常就足够了。
如果以上都不适用,并且由于某些原因我仍然希望并行处理,那么我希望尽可能地启动一个独立的进程。
多线程是一个坏主意,如果:
- 多个线程访问和更新相同的资源(设置变量,写入文件),我们不了解线程安全性。
- 多个线程相互交互,我们不了解互斥锁和类似的线程管理工具。
- 程序使用静态变量(默认情况下,线程通常共享它们)。
- 我们尚未调试并发问题。
线程问题的常见根源是用于同步数据的常用方法。让线程共享状态,然后在所有适当的位置实现锁定是设计和调试复杂性的主要来源。获得锁定权以平衡稳定性,性能和可伸缩性始终是一个很难解决的问题。即使是最有经验的专家也会经常出错。处理线程的替代技术可以减轻这种复杂性。 Clojure编程语言实现了一些用于处理并发的有趣技术。
多线程是不好的,除非在单线程情况下是好的。这种情况是
- 作品是CPU绑定,或者部分是CPU绑定
- 这项工作是可并行的。
如果缺少这两个条件之一或者全部,则多线程将不是一个成功的策略。
如果工作不受CPU限制,那么我们不是在等待线程完成工作,而是等待某些外部事件(例如网络活动)来完成该过程。使用线程,会在线程之间增加上下文切换的开销,同步的开销(互斥对象等)以及线程抢占的不规则性。最常见的替代方法是异步IO,其中单个线程侦听多个io端口,并在现在碰巧准备就绪的任何一个上进行操作,一次一次。如果偶然地所有这些慢速通道都同时准备就绪,则似乎我们会遇到速度变慢的情况,但实际上这很少是真的。单独处理每个端口的成本通常与空多个通道时同步多个线程上的状态的成本相当或者更好。
许多任务可能是受计算限制的,但使用多线程方法仍然不切实际,因为该过程必须在整个状态上进行同步。这样的程序无法从多线程中受益,因为无法同时执行任何工作。幸运的是,大多数需要大量CPU的程序都可以并行化到某个级别。
多线程具有可伸缩性,可以让UI保持其响应能力,同时在后台执行非常复杂的事情。我不了解其他响应在哪里获取有关多线程的信息。
当我们不应该使用多线程时,这是对问题的误导性问题。问题是这样的:为什么我的应用程序进行多线程处理会导致串行/以太网通信失败?
该问题的答案将取决于实现方式,应在另一个问题中进行讨论。我知道一个事实,即可以在多线程应用程序中同时进行以太网和串行通信,而无需执行任何其他任务,而不会造成任何数据丢失。
不使用多线程的原因之一是:
- 只有一项任务,没有用户界面会干扰任务。
使用多线程的原因有:
- 为用户提供出色的响应能力
- 同时执行多个任务以减少总体执行时间
- 使用更多的当前多核CPU,以及将来的多核CPU。
多线程编程的三种基本方法可以轻松实现线程安全,我们只需使用一种方法即可成功:
- 线程之间传递的线程安全数据类型。
- 线程对象中的线程安全方法用于修改在数据之间传递的数据。
- PostMessage在线程之间进行通信的功能。