[原]多线程多少算多?
最近在抓软件的性能优化。团队里有位同学指出,我们的程序一启动就创建了60个线程,太多了,应该控制一下。也有同学提出不同意见,说线程多不是问题,别把它当成指标,盯住内存、CPU才是正经。使用多线程,为的是提高执行效率;那么,是不是线程越多越好呢?
假设我们有100个下载任务,我们可以有以下3种实现方法:
- 使用一个线程,依次执行100个下载任务;
- 使用100个线程,每个线程执行一个下载任务;
- 使用10个线程,每个线程依次执行10个下载任务。
哪种实现方案更好呢?别急!我们先来理一理两个基本概念:进程与线程。我发现 这篇文章总结得言简意赅:
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
……
简而言之,一个程序至少有一个进程,一个进程至少有一个线程……对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程…
然而,使用线程是有成本的。我们先来看一下线程在创建和销毁时的开销:
- 在创建线程时,系统会分配并初始化一个线程内核对象,并且保留1MB的堆栈空间。Windows还会调用当前进程中的每个DLL的入口函数(参数为DLL_THREAD_ATTACH),通知它们新建了一个线程。
- 当线程将要被销毁时,Windows也会调用当前进程中的每个DLL的入口函数(参数为DLL_THREAD_DETACH),通知它们该线程即将离去。随后,线程的内核对象以及之前分配的堆栈空间也会被释放。
再来看线程的执行过程。使用多线程可以实现多个任务的并行。这个很棒!但我们还需要理解Windows是怎么来实现多任务并行的。我们已经知道,线程是CPU调度的基本单位;对于单核的CPU来说,在某一时刻只有一个线程可以运行。系统怎么让所有线程雨露均沾呢?Windows的做法是,把CPU时间切成小片后再按需分配。Windows会不停地跟踪记录每一个线程对象,大约每隔20毫秒,决定CPU接下来调度哪一个线程使其运行。
对于上图中的三个线程,在CPU中执行的顺序可能是A1B1C1…A2B2C2…A3B3C3… 而在线程A切换到线程B执行时,Windows需要做线程的上下文切换(Context switch),使CPU停止执行当前线程的代码,转而开始执行另一个线程的代码。
上下文切换的过程大致如下( 参考文章):
- 进入内核模式。
- 将CPU的寄存器保存到当前正在执行的线程的内核对象中。
- 需要一个自旋锁(spin lock),确定下一次调度哪一个线程,然后再释放该自旋锁。
- 如果下一次调度的线程不属于同一个进程,那么此处开销更大,因为OS必须先切换虚拟地址空间。
- 把即将要运行的线程的内核对象的地址加载到CPU寄存器中。
- 退出内核模式。
在用户模式与内核模式之间切换的系统开销不小,如果线程很多,线程之间频繁切换,开销自然更不可忽视。线程的整个生命周期对于系统来说都是一种负担。因此,线程不是越多越好!线程的数量必须是有节制的。每增加一个线程,我们都需要去评估ROI,力求平衡!对于单核CPU来说,假设在系统中没有运行其他进程的情况下,使用多线程完成A、B、C三个任务的总时间,与使用一个线程依次完成三个任务的时间其实是相当的;考虑到线程上下文切换的CPU消耗,前者花的时间反而更多。而在多核CPU的情况下,线程可以分散调度到不同的核上,多线程设计能够发挥多核的优势,当然也能显著地提升执行效率。不过,多线程多少算多?依然是个问题。
回顾本文开头那个关于下载的应用场景。显然,只使用一个线程不见得不好。但是,简单粗暴地为每个下载任务都创建一个线程肯定是不足取的!我们应该对线程池做更精致的设计,至于池子里实际使用10个线程还是20个线程,这倒是未必的!