创建线程时有多少开销?

66

我刚刚审查了一些非常糟糕的代码,这些代码通过创建新线程来在串行端口发送消息,为每个发送的消息创建一个新线程来打包和组装消息。是的,针对每个消息都会创建一个pthread,正确设置位,然后线程终止。我不知道为什么有人会这样做,但它引发了一个问题 - 实际上创建线程时有多少开销?


单个工作线程可以帮助解决多线程可能出现的某些资源争用问题(如交替写入等)。 - clstrfsck
我想评论一下,是的,这可能是一个可怕的事情 - 这引出了我的问题,创建线程时会产生多少开销(通常情况下)?坦白地说,我不知道如何确定或甚至测量pthread库的实现。 - jdt141
16
如果消息大小合适,则开销为0.37%。 - Lutz Prechelt
@LutzPrechelt 你能解释一下你是如何得出那个数字的吗? - undefined
1
@戴:是的,我在开玩笑,有点儿:原始问题缺乏关键信息。没有关于串口传输速率和消息长度的信息,无法给出正确的答案。 - undefined
显示剩余3条评论
12个回答

81
为了挽救这个旧帖子,我刚做了一些简单的测试代码:
#include <thread>

int main(int argc, char** argv)
{
  for (volatile int i = 0; i < 500000; i++)
    std::thread([](){}).detach();
  return 0;
}

我用 g++ test.cpp -std=c++11 -lpthread -O3 -o test 对其进行了编译。然后在一个旧的(内核为2.6.18),正在进行数据库重建的低速笔记本电脑(Intel Core i5-2540M)上连续运行了三次。三次连续运行的结果分别是:5.647秒、5.515秒和5.561秒。因此,在这台机器上,每个线程大约需要10微秒,可能在您的机器上要少得多。

考虑到串行端口的最大速率约为每10微秒1比特,这并不是什么大的开销。当然,有各种其他的线程损失,比如传递/捕获参数(虽然函数调用本身也会带来一些影响),缓存在核心之间的减速(如果在不同核心上的多个线程同时争夺相同的内存),等等。但总的来说,我非常怀疑您提出的用例会对性能产生负面影响(并且可能会带来好处),尽管您已经预先将这个概念标记为“真的很糟糕的代码”,甚至不知道启动线程需要多长时间。

是否使用它取决于您的具体情况。调用线程还负责什么?准备和写出数据包涉及到什么?它们的写出频率是多少(以何种方式分布?均匀分布、聚集分布等等...)以及它们的结构如何?系统有多少个核心?根据具体情况,最佳解决方案可能从“根本不需要线程”到“共享线程池”到“每个数据包一个线程”任何地方都可以。

请注意,线程池并非魔法,有些情况下,它们会比独立的线程慢,因为线程的最大减速之一是同步缓存内存,而同时必须从不同的线程中查找并处理更新的线程池必须这样做。因此,如果处理器不确定其他进程是否改变了某个内存部分,则主线程或子处理线程中的一个可能会被卡住等待。相比之下,在理想情况下,对于给定任务的唯一处理线程只需要与其调用任务共享内存一次(在启动时),然后它们再也不会相互干扰。


15
作为一个Windows用户,我偶然发现了这个帖子,并且对我的系统表现很感兴趣。我使用msvc进行编译,启用标准的发布优化,在6700k上运行,总共需要31.442秒才能完全运行。我唯一做的修改是在循环前后添加了std :: chrono :: high_resolution_clock + time_points,并在退出之前使用std :: cout输出结果。结果令人震惊。我尝试使用mingw-w64的7.1.0 g ++和您完全相同的命令行参数进行编译,但它在几秒钟后崩溃,所以不知道出了什么问题,同样适用于我手头的clang ++ v8.0。 - Mark A. Ropper

20

一直以来我听说创建线程是非常便宜的,尤其是与创建进程的替代方案相比。如果您所讨论的程序没有很多需要同时运行的操作,则可能不需要使用线程,根据您的描述,这很可能是情况。一些支持我的文献:

http://www.personal.kent.edu/~rmuhamma/OpSystems/Myos/threads.htm

从以下几个方面看,线程是一种廉价的方式:

  1. 他们只需要一个堆栈和寄存器存储空间,因此线程创建成本低。

  2. 线程在工作的操作系统中使用非常少的资源。也就是说,线程不需要新的地址空间、全局数据、程序代码或操作系统资源。

  3. 当与线程一起工作时,上下文切换速度很快。原因是我们只需要保存和/或恢复PC、SP和寄存器。

更多类似内容请点击这里.

《操作系统概念第8版》(第155页)中,作者写到了使用线程的好处:

为进程分配内存和资源是昂贵的, 因为线程共享所属进程的资源,创建和上下文切换线程更经济。在实际中,很难量化开销的差异,但通常创建和管理进程比线程耗时更长。例如,在Solaris中,创建进程的速度大约比创建线程慢30倍,上下文切换慢五倍。


7
但是另一种选择可能是单个重用线程或线程池,而不是进程。 - Matthew Flaschen
4
实际上,进程创建比线程创建更便宜。进程创建中的 fork 部分基本没有成本,因为内存页面在硬件级别被复制。请参阅 Google Chrome 团队发现的内容:http://www.hanselman.com/blog/MicrosoftIE8AndGoogleChromeProcessesAreTheNewThreads.aspx - Martin York
1
@Martin York 根据我的教材(和我的教授)所说,进程的创建比线程的创建更加昂贵。我会添加一段来自我的教材的内容,这样您可以自行判断。 - ubiquibacon
1
@Martin York 这篇文章似乎在说,由于“摩尔定律不断发展”,进程创建比以前更快了,但这并没有将进程和线程放在同等的平台上进行比较。如果进程创建速度提高了一个数量级,那么线程创建速度也应该提高。据我所知,创建进程永远不可能比创建线程更快,因为进程中始终会有更多的东西。 - ubiquibacon
1
@Martin York您提供的链接没有日期,但最新的参考资料是1990年。我的书已经第8版,并于2009年获得版权。我的书的原始版本发表于1985年,比您提供的链接中大多数参考资料都要新。您提供的链接没有比较进程和线程,事实上它甚至没有提到线程。我完全同意您最近的评论,但我仍然认为(基于我所学和我的参考资料)进程创建将始终比线程创建更昂贵。 - ubiquibacon
显示剩余14条评论

14

线程创建会有一些开销,但与串口的通常较慢的波特率(19200比特/秒是最常见的)相比,这并不重要。


5
我同意。当网络可能会引起数十毫秒甚至几秒钟的延迟时,为什么要担心创建线程的微秒级延迟。 - JSON

13
…在串口上发送消息…每个消息都会创建一个pthread,按比特正确设置,然后线程终止。…实际创建线程时有多少开销?
这高度依赖于系统。例如,上次我使用VMS线程是非常慢的(已经过了几年了,但从记忆中来看,一个线程每秒只能创建大约10个(如果没有线程退出,几秒钟后你会核心转储)),而在Linux上,您可能可以创建数千个。如果想要确切的答案,请在您的系统上进行基准测试。但是,仅仅知道这些并不对了解消息更多的信息有用:它们是否平均为5个字节还是100k,它们是否连续发送或者在线路空闲之间等待,在应用程序的延迟要求方面都与代码线程使用的适当性一样相关,任何关于线程创建开销的绝对测量。性能可能不需要成为主要的设计考虑因素。

12

你绝对不想这样做。创建一个单一的线程或一个线程池,当有消息可用时只需发送信号。接收到信号后,线程可以执行任何必要的消息处理。

就开销而言,线程的创建/销毁,特别是在Windows上,是相当昂贵的。具体来说,大约需要数十微秒的时间。它应该主要在应用程序的开始/结束时完成,可能会有动态调整大小的线程池的例外情况。


3
是的,一个“永久”的专用工作线程也可以解决可能出现的多线程问题。 - ruslik
@MichaelGoldshteyn:你有想法如何用Python实现这个吗?(http://stackoverflow.com/q/33453581/2284570) - user2284570

8
我曾经在一个VOIP应用程序中使用了上述“可怕”的设计。它非常有效......对于本地连接的计算机,绝对没有延迟或丢失/丢弃包。每当数据包到达时,就会创建一个线程并将数据传递给输出设备进行处理。当然,这些数据包很大,所以不会造成瓶颈。同时,主线程可以回路等待并接收另一个传入的数据包。
我尝试过其他设计,在其中需要预先创建我需要的线程,但这会带来自己的问题。首先,您需要为线程正确设计代码,以按确定性方式检索传入的数据包并处理它们。如果您使用多个(预分配的)线程,可能会导致数据包被“乱序”处理。如果您使用单个(预分配的)线程来循环并拾取传入的数据包,则有可能该线程遇到问题并终止,从而没有线程来处理任何数据。
为每个传入数据包创建一个线程非常干净,特别是在多核系统和传入数据包较大的情况下。此外,更直接地回答您的问题,与创建线程相反的选择是创建运行时进程来管理预分配的线程。能够同步数据交接和处理以及检测错误可能会增加与仅简单地创建新线程一样多或更多的开销。这完全取决于您的设计和需求。

这没有任何意义。如果一个线程崩溃了,你的整个应用程序都会崩溃,除非在你的操作系统上它是可捕获的,在这种情况下,你可以利用这个机会来生成一个新的线程。如果不是直接崩溃,而是异常/错误代码,线程可以捕获它并采取适当的措施。 - Joseph Garvin

7

创建线程和在线程中进行计算非常昂贵:所有数据结构都需要设置,线程必须向内核注册并进行线程切换,以便新线程实际上得到执行(在未指定且不可预测的时间内)。执行thread.start并不意味着立即调用线程主函数。

正如 typoking 提到的文章指出,与进程创建相比,线程的创建仅较为廉价。总体而言,它是相当昂贵的。

我永远不会使用线程

  • 进行短时间计算
  • 需要计算结果在代码流中使用的计算(这意味着,我启动线程并等待它返回其计算结果)

在您的示例中,创建一个处理所有串行通信并永久存在的线程是有意义的(正如已经指出的那样)。


1
踩票让人困惑。了解上下文切换的成本是任何线程成本讨论的核心。 - Abtin Forouzandeh
1
你的第二个要点似乎排除了异步 - 尽管我猜这是因为异步是C++11,而你的答案是在'10年写的。 - Matt Parkins

6

为了比较,我们来看一下OSX:链接

  • 内核数据结构:大约1 KB 堆栈空间:512 KB (次要线程):8 MB (OS X主线程),1 MB (iOS主线程)

  • 创建时间:大约90微秒

我猜posix线程的创建时间应该也是这个范围内。


3
在任何理智的实现中,线程创建的成本应该与涉及的系统调用数量成比例,并且与类似于openread这样熟悉的系统调用的数量相同。我在我的系统上进行了一些随意的测量,显示pthread_create所需的时间大约是open(“/dev/null”,O_RDWR)的两倍,这与纯计算相比非常昂贵,但与涉及用户和内核空间之间切换的任何IO或其他操作相比则非常便宜。

在我的情况下,这将涉及创建数千个线程。有没有一种方法可以在Python中避免这种情况?http://stackoverflow.com/q/33453581/2284570 - user2284570

3
正如其他人所提到的,这似乎非常依赖于操作系统。在我运行Win10的Core i5-8350U上,它花费了118秒,这表明每个线程的开销约为237微秒(我怀疑病毒扫描程序和所有其他垃圾IT安装也在拖慢速度)。双核Xeon E5-2667 v4运行Windows Server 2016花费41.4秒(每个线程82微秒),但它也在后台运行许多IT垃圾,包括病毒扫描程序。我认为更好的方法是实现一个队列,并使用一个线程连续处理队列中的内容,以避免每次创建和销毁线程的开销。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接