上下文切换的资源消耗
当大家在设计高并发服务的时候,多线程的模型已经成为主流设计。很多人心中都有这样一个常识:线程之间的上下文切换的成本是很昂贵的,开上千个线程对内存的压力很大,那么这里我们一起来分析一下,到底,上下文切换要花多久时间,占用多少资源。
Linux线程和NPTL
在2.6版本之前,Linux对线程支持的不好,主要靠的是在进程上做一些hack来实现线程。在Futexes出现之前,既没有低延时的同步方案(当时一般使用信号来进行同步),也不能很好的利用多喝系统的能力。
在2005年,红帽的Ulrich Drepper和Ingo Molnar两位大佬提出了Native Posix Thread Library,简称NPTL,并把NPTL整合进了Linux 2.6。有了NPTL,线程的创建时耗快了7倍,通过使用futexes,同步操作也变得非常的快。内核更加重视使用多核硬件的能力,将线程和进程变得更轻量。与此同时内核还加入了一个更高效的调度器:O(1) scheduler。
上下文切换时会发生什么?
首先,在上下文切换会切换到内核态。这里会需要保存线程的寄存器,并且跳转到内核代码。切换可能是通过一个显式的系统调用,或者是时间中断,也就是内核抢占了一个用户线程进行调度。
然后,调度器开始决定接下来执行哪个线程。选定线程之后,需要把新线程的page tables加载到内存中。
最后,内核恢复新线程的寄存器,切换回用户态。
上下文切换的成本
要准确的测量两个线程进行上下文切换的开销,我们需要小心的触发切换,避免额外的工作。这样可以测量切换的直接成本。实际上上下文切换还存在间接的成本,每个线程都有一组自己占用的内存,其中一部分可能在cache中,当我们切换到其他进程的时候,这些cache数据会慢慢的被踢出。在线程之间频繁的切换会导致cache数据反复变化,降低命中率。
本文要测量的成本,只考虑直接成本。我们创建两个线程,线程间通过一个pipe发送小量的数据。读线程在进行read
时会阻塞,这时操作系统切换到写线程。我们再单独记录一下写数据需要占用多久时间,将总时间减去写数据的时间,就大概得出了上下文切换消耗的时间了。在我的笔记本上,这个时间大约是1.3微秒。
上下文切换还有一个隐性的成本,在多核机器上,内核有时候会将一个线程在多个核之间迁移,原因是这个线程之前使用的核还被别的线程占用了。内核的这种操作可以更加充分的利用多核系统,但是因为核有自己的cache,这样也会一定程度上增加开销。我们可以使用taskset
命令来强制程序使用一个核,这时,我们测出来上下文切换大约是0.9微秒。
1微秒这个结果说明了一个问题,在现代的linux系统中,上下文切换非常的快,比我们绝大部分的业务代码都快,所以上下文切换不应该成为性能设计时候的理论瓶颈。
线程使用的内存
使用多线程来设计系统时,除了上下文切换的开销之外,大部分人还会担心线程很多的时候,对内存的占用情况。同一个进程的多个线程会共享内存,但是每个线程还会有一些私有的空间,其中stack就是很大的一部分。
每个线程的默认栈空间是8MB,可以通过ulimit -s
命令来查看。
我们可以写一个程序,来创建很多个线程,然后观察内存占用,在我的笔记本上,创建10000个线程后,通过htop命令我观察到,virtual memory使用了80G,resident memory试用了240M,这两个指标的区别在哪呢?我的笔记本内存只有8G,虚拟内存为什么能用出80G呢?
首先我们来看看Virtual memory和Resident memory分别是什么东西
当Linux程序使用malloc
申请内存的时候,其实并没有真的开辟一片内存区域给程序,只是在操作系统的一个表里记录了一下。只有当程序真的开始访问内存的时候,才会使用到RAM。这时virtual memory的基础。
因此,一个进程使用的内存有两种意思,一个是它占用的虚拟内存有多大,另一个是它实际占用的RAM有多大。前者几乎没有边界,而后者的上限就是物理内存的大小(超过之后还可以用硬盘,但是会严重拖慢性能)。实际使用的物理内存,就是resident memory。
所以我们应该更关心线程任务实际使用的内存,而不是担心创建线程所带来的内存消耗
结论
在现代的操作系统中,线程上下文切换的开销很小,单纯线程本身的内存开销也不大,我们应该更多的关注自己代码的性能以及内存消耗。
版权声明: 本文为 InfoQ 作者【麻瓜镇】的原创文章。
原文链接:【http://xie.infoq.cn/article/6f82a6b154cd7476036efabef】。文章转载请联系作者。
评论