
线程与线程池原理全解析:打破单线程误区,从底层讲透转载请注明出处:本文从底层原理出发,把JS多线程和线程池讲得明白,看完再也不会被"JS单线程"的说法误导。一、先搞懂:JS到底是不是单线程?几乎所有入门教程第一句都会告诉你:JS是单线程语言。但这句话其实只说了一半,很多人学了好几年都没搞懂另一半。1.1 单线程到底指什么?我们说JS单线程,指的是JS的主线程是单线程:你写的JS业务代码,永远是在主线程上串行执行的DOM操作、页面渲染、事件循环调度,都在主线程上跑同一时间主线程只能干一件事,前一个任务没执行完,后面的任务只能等着这就是为什么你写个死循环,页面直接就卡了——主线程被占住了,渲染、交互都停了。1.2 另一半真相:JS运行时是多线程的但是!JS的运行环境(浏览器/Node.js)本身是多线程的,你每天都在用多线程,只是你自己不知道:你发个AJAX请求,网络请求是在专门的网络线程里跑的,不是主线程在等你读个本地文件,IO操作是在系统线程里跑的定时器计时、事件监听、图片解码,全都是在单独的线程里跑的举个最通俗的例子:你开了个奶茶店,你自己是收银员(主线程),负责点单收钱。你不可能点完单自己去做奶茶、煮珍珠、送外卖,那样后面的顾客全堵死了。所以你雇了几个后厨员工(其他线程)专门做这些事,你只负责点单,后厨做好了告诉你一声,你把奶茶给顾客就行。这就是JS的线程模型:主线程当收银员做核心调度,其他脏活累活丢给别的线程干,干好了通知主线程。单线程的是你这个收银员,整个奶茶店(运行时)是多线程的。1.3 为什么JS一开始要设计成主线程单线程?很多人觉得单线程是JS的缺陷,其实这是故意设计的,最核心的原因有两个:避免DOM操作冲突:如果JS是多线程,两个线程同时改DOM,一个要加节点一个要删节点,页面直接就乱了,还要加锁、同步,复杂度直接上天。单线程从根源上避免了这个问题。降低语言门槛:JS刚出来的时候就是给网页做简单交互的,要是一开始就搞多线程,光线程同步、死锁这些概念就能劝退90%的开发者,根本不可能有今天的生态。二、单线程的问题:为什么我们需要多线程?单线程好是好,但有个致命问题:阻塞。主线程就像收银员,要是遇到个顾客点了100杯奶茶,你在那算钱算5分钟,后面所有顾客都得等着,店直接就堵死了。对应到JS里,就是CPU密集型任务:比如100万次循环计算、大JSON解析、图片压缩、加密解密,这些任务放在主线程跑,直接就会导致:事件循环停转,所有异步回调都没法执行页面渲染卡住,白屏、掉帧用户点击、滑动完全没反应这时候就需要用户可控的多线程了:浏览器有Web Worker,Node.js有Worker Threads,都是干这个的——把这些耗CPU的计算任务丢给单独的线程跑,主线程该干嘛干嘛,算完了把结果传回来就行。Web Worker的本质是什么?很多人以为Web Worker是什么黑科技,其实就是一个独立的JS运行环境:每个Worker都是操作系统分配的独立线程,和主线程完全并行有自己独立的V8实例、独立的内存空间、独立的事件循环和主线程完全隔离,不能共享变量,不能访问DOM、window这些主线程的东西唯一的通信方式就是postMessage发消息,底层把数据复制一份传过去就相当于你给后厨单独开了个操作台,他在那自己做奶茶,做好了喊你一声,你递给他的材料都是复制一份给他,他那边改了不影响你这边的材料,从根源上避免冲突。浏览器里除了最常用的普通Worker,还有两种特殊Worker:SharedWorker:多个标签页可以共用同一个Worker,比如几个标签页共享一个WebSocket连接,不用每个页都开一个ServiceWorker:后台常驻的线程,就算你把网页关了它还能跑,用来做离线缓存、消息推送这些Node.js里的两套线程模型Node.js比浏览器多一层,有两套完全独立的线程池,很多人搞混:libuv底层线程池:这个是Node自带的,你平时用的fs读文件、dns解析、crypto加密,全都是丢给这个线程池跑的,你写代码的时候感知不到,它默认开4个线程,专门处理系统层面的阻塞IO操作,不跑你的业务JS代码。Worker Threads:这个和浏览器的Web Worker是一个东西,专门给你跑业务JS里的CPU密集任务的,独立V8实例,和主线程并行,Node里还支持共享内存,通信比浏览器更方便。一句话区分:libuv线程池是Node自带的后厨,专门做系统层面的杂活;Worker Threads是你自己雇的员工,专门帮你做业务里的重计算。三、线程池原理:为什么不能想开多少线程就开多少?很多人第一次接触多线程的时候会想:那我多开点线程不就快了?来一个任务开一个线程,岂不是美滋滋?要是真这么干,你的程序直接就崩了。这就是为什么要有线程池。3.1 开一个线程的成本比你想的大得多你以为开线程就是new一下的事?实际上创建一个操作系统线程是非常重的操作:内核开销:创建线程要进内核态,分配内核栈、线程ID、进程控制块,这个开销比普通函数调用大上千倍内存开销:每个线程默认要占1M左右的栈内存,你开1000个线程就是1G内存,直接就OOM了CPU上下文切换开销:CPU核心数是有限的,你电脑一般也就4核8核,开几十上百个线程,CPU根本忙不过来,要不停在不同线程之间切换,每次切换都要保存寄存器、栈、程序计数器,切换开销占了一半以上的时候,反而比单线程还慢。就像奶茶店,你本来就10平米的店,你雇20个后厨,人挤人转都转不开,不仅做不快,还容易出事,房租(内存)还贵死。3.2 线程池到底是个什么东西?线程池的思路特别简单:提前雇固定数量的员工,任务来了排队,谁闲下来谁做,做完一个接下一个,不随便招人也不随便裁人。本质上就是三个部分:固定数量的工作线程:一开始就创建好N个线程,一直跑着循环等任务,不随便销毁任务队列:所有要执行的任务先丢到队列里排队,先进先出调度逻辑:哪个线程闲下来了,就从队列里拿第一个任务给它做,做完了再拿下一个这样做的好处太明显了:不用频繁创建销毁线程,省了大量开销线程数量固定,不会把内存占满,也不会把CPU切换搞崩任务来了直接用空闲线程,不用等线程创建,响应更快所有线程统一管理,出问题好处理3.3 线程池是怎么调度任务的?工业级的线程池调度逻辑基本都是一样的,我们简单说下原理:来新任务了,如果现在闲的线程够,直接给空闲线程做如果线程都在忙,就把任务丢到队列里排队如果队列也满了,就临时多开几个线程帮忙(最多开到设置的最大值)如果线程数已经到最大值了还忙不过来,就触发拒绝策略(直接报错、丢任务、让提交任务的线程自己做等等)等忙完了,临时开的线