js单线程如何实现并发(单核和多核,单进程和多进程,单线程与多线程)

本文目录
单核和多核,单进程和多进程,单线程与多线程
之前我对单进程、多进程、单线程、多线程,单核、多核、并发、并行;总是没有详细的认知;因此准备归纳总结下,加强自己的记忆。 (这里的并发不是网络并发,请区别对待)
我们先理解下进程在 CPU 中的并行方式:
进程执行需要系统资源,但是不同的进程间是无法共享 CPU ,怎么样才能共享资源和 CPU 呢? 线程 (有些程序中叫 协程 )的概念就出现了,线程是被包含在进程中,同一进程中的不同线程可以共享 CPU 和 程序上下文 (进程被分配到的资源)
在单核单 CPU 中,在同一个进程中的不同线程想要使用 CPU 核心,则会进行线程切换,但是由于共享了程序执行环境,这个线程切换会比进程切换开销小很多。注意:由于 CPU 是单核的,所以同时刻只能执行一个线程,所以说这里依然是并发。
如果我们的 CPU 是多核的呢?那么进程中的不同线程就可以同时运行了,真正的 并行 就出现了
文章的开头我们提到过 线程是CPU调度和分配的基本单位 。要和 进程是操作系统进行资源分配的最小单位 区分;假设我是 CPU ,操作系统调度器将一个进程分配给我之后,我拿到进程后,能看到什么,是不是只能看到进程中的多个线程,那么我现在能调度和分配的是不是只能是线程。如果我是4核的,把线程 ABCD 分配到核心1234,其他的线程依然要等待分配。所以线程是 CPU 调度和分配的基本单位。
单 CPU 中进程只能是并发,多 CPU 中进程可以并行。
单 CPU 单核中线程只能并发,单 CPU 多核中线程可以并行。
理解了上面这些关系,再去看看 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理 ,思路会更加清晰
7.单线程并发
单线程并发 意味着貌似可以在单个线程中同时完成多个任务。 从表面上看,单线程并发听起来有点矛盾。 以前,在多线程体系结构中,多个任务将在多个线程之间分配,以并行执行。 因此,不同任务之间的切换是通过操作系统和CPU在不同线程之间的切换来完成的。 但是,单个线程实际上可以几乎同时处理多个任务。 在本单线程并发教程中,我将解释单线程并发如何设计的,以及有何好处。请注意:本教程仍在进行中。 在不久的将来会添加更多!
请注意:本教程仍在进行中。 在不久的将来会添加更多!
在经典的多线程体系架构中,通常将每个任务分配给一个单独的线程以执行。 每个线程一次只执行一个任务。 在某些设计中,将为每个任务创建一个新线程,因此一旦任务完成,该线程就会死掉。 在其他设计中,线程池保持活动状态,该线程池一次从任务队列中执行一个任务,然后执行另一任务,如此往复。有关更多信息,请参阅我的 线程池 教程。
多线程体系架构的优点是,相对容易地在多个线程和多个CPU之间分配工作负载。 只需将任务分配给线程,然后让OS / CPU将线程调度到CPU。
但是,如果正在执行的任务需要共享数据,则多线程体系架构可能会导致许多并发问题,例如 竞态条件 , 死锁 , 饥饿 , 滑动条件 , 嵌套监视器锁定 等。通常,越多的线程共享相同的数据和数据结构,发生并发问题的可能性就越高。 换句话说,您需要在设计时留意更多内容。
当多个线程试图同时访问同一个数据结构时,经典的多线程体系结构有时还会导致拥塞。 这取决于给定数据结构的实现方式,某些线程可能会被阻塞,以等待其他正在访问该数据结构线程访问完成。
经典多线程体系结构的替代方法是单线程或 同线程 。 通过仅使用一个线程来执行应用程序中的所有任务,就可以完全避免前一部分(经典的多线程并发架构)中列出的所有并发问题。
您可以扩展单线程体系结构以使用多个线程,其中每个线程的行为就像是一个单独的隔离的单线程系统。 在那种情况下,我将此架构称为相同线程。 执行任务所需的所有数据仍保持隔离在单个线程内-在同一线程内。
如果只有一个线程执行应用程序的所有任务,则可能会导致一些问题:
● 从任务中阻止IO操作,将阻止线程,从而阻止整个应用程序。
● 长时间运行的任务,可能会产生无法接受的延迟其他任务的执行。
● 单个线程只能使用到单个CPU。
可以解决这些问题,但又不会失去单线程并发体系结构的简单性的优势,也不会使整体设计过于复杂。
大多数长时间运行的应用程序以某种循环执行,其中应用程序主线程正在等待来自应用程序外部的输入,处理该输入,然后返回等待状态。
这种线程循环在服务器应用程序(Web服务,服务等)和GUI应用程序中都可以使用。 有时,您可以看到该线程循环, 而有时则看不到。
您可能会想知道,在一遍又一遍地密集循环中,执行的线程是否会浪费大量CPU时间。 如果线程在运行时没有任何实际工作要做,那么可能会浪费掉大量CPU时间。 因此,如果执行循环的线程判断出休眠几毫秒是可行的,则可能会使用“休眠”,而非循环, 这样可以减少CPU的时间浪费。
线程循环通常在其生命周期内执行两种类型的任务:
● 重复任务
● 一次性任务
以下各节将对这两项任务进行更详细的说明。
重复任务是一个重复执行的任务,它在执行该任务的线程的生命周期内一次又一次地执行。 通常,对于任务的每次调用,将完全执行重复的任务。
重复任务的一个示例是检查一组入站网络连接上的传入数据。 如果检测到任何传入数据,将对其进行处理,并且在处理之后,将针对此特定调用执行重复的任务。 但是,需要一次又一次地检查入站数据,以使应用程序能够连续响应传入的数据。
一次性任务是只需要执行一次的任务。一次性任务可以是短期运行,也可以是长期运行。
短时任务是一个足够短的任务,可以在一个执行阶段中完成,而又不会使执行该任务的线程因该线程承担的其他职责(它必须执行的其他任务)而延迟。
一次性长时间运行的任务是在单个执行阶段中花费太长时间才能完成的任务。 “花费太长时间”是指执行任务中的全部工作量将占用太多的线程时间,因此其他重复任务或一次性任务将被延迟太多,以至于应用程序的总响应速度受到伤害。
为了避免单个长时间运行的任务占用过多的线程执行时间,将完成任务所需的全部工作分解为较小的块,可以一次执行一个块。每个块必须足够小,以免延迟线程执行过多任务所需的其他任务。
长时间运行的任务在内部跟踪其执行块。执行长时间运行的任务的线程将多次调用其执行方法,直到所有任务块均已完全执行。在调用特定长时间运行任务的执行方法之间,线程可以调用其他长时间运行任务,其他重复任务或线程承担的任何职责的执行方法。
一次性的长期运行任务可能是处理目录中的N个文件。可以将N个文件的处理分解成较小的块,而不是在单个执行阶段中处理所有N个文件,而每个块都在单个执行阶段中进行处理。例如,每个执行阶段可以处理1个文件。要处理所有N个文件的任务
在线程循环中,一次性任务通常由重复任务检测并执行,如下所示。
为了能够似乎同时在一个以上的任务上取得进展,在任务上取得进展的线程必须能够在这些任务之间进行切换。 这也称为任务切换。
任务切换的确切工作方式取决于任务的类型-线程是在重复任务还是一次性任务之间进行切换。 虽然总的原理还是一样的。 我将在以下各节中对这两者进行更详细的说明。
重复的任务通常只有一个方法,该方法被同一线程重复调用。 重复任务是应在应用程序的整个生命周期中重复的任务,因此它永远不会真正“完成”。 重复的任务执行了所需的操作,然后退出其执行方法,将控制权交还给调用线程。
通过以循环方式调用它们的执行方法,单个线程可以在多个重复任务之间进行切换。 首先重复执行的任务A有执行的机会,然后是B,然后是C,然后是A,依此类推。
万一重复任务没有完全完成它开始的任何工作,它可以记录它在内部走了多远,并在下次调用重复任务时从那里继续。
一次性任务与重复任务的不同之处在于,一次性任务有望在某个时间点完成。 这意味着,有时需要从任务池中删除一次性任务。
除此之外,一次完成任务之间的切换类似于重复任务之间的切换。 执行线程调用给定的一次性任务的执行方法,该任务在短时间内取得进展,然后在内部记录其执行的距离,然后退出其执行方法,将控制权交还给调用线程。 现在,调用线程可以循环方式调用任务池中的下一个一次性任务。
每次调用一次性任务的执行方法后,调用线程将检查任务是否已完成。 如果已删除,则一次性任务将从任务池中删除。
在实践中,一个应用程序可能包含一个调用一个或多个重复任务的线程循环,重复任务可以执行一次任务作为重复行为的一部分。 下图说明了这一点。 该图仅描述了一个重复的任务,但根据具体应用,可能还会有更多任务。
当单个线程要在多个任务(无论是重复任务还是一次性任务)之间切换时,必须确保在一次调用任务时,这些任务不会占用过多的线程执行时间。换句话说,确保每个任务之间执行时间的公平平衡是每个任务帮助的职责。
任务应该允许自己执行多长时间,具体取决于系统设计者。对于一次性任务,这可能会有些复杂。有些任务自然很快就完成了,而另一些任务自然要花费更长的时间才能完成。对于运行时间较长的任务,由任务的实现者来估计如何将工作分解为足够小的分区,以便可以在不延迟其他任务过多的情况下执行每个分区。
需要注意的一件有趣的事是,如果线程以循环方式调用每个一次性任务,那么任务执行器包含的一次性任务越多,每个线程获得的执行时间就越短,因为在执行任务之前需要更长的时间接下来的执行时间。
可以实现一个将某些任务优先于其他任务的任务执行器。 例如,任务执行者可以在内部将任务保存在不同的列表中,例如 执行低优先级任务列表中的任务每执行1次,就执行2次高优先级列表中的任务。
确切地说,如何执行优先任务执行器将取决于具体需求。 还有多少个优先级,例如 低/高,或低/中/高等
如果一次性任务正在等待某些异步操作完成,例如 如果来自远程服务器的答复,则一次性任务将无法继续进行下去,直到它正在等待的异步操作完成为止。 在那种情况下,一次又一次地调用该任务可能没有意义,只是为了使该任务意识到它无法取得任何进展并立即将控制权返回给调用线程。
在这种情况下,一次性任务能够将自己“停放”在任务执行器内部可能是有意义的,因此不再被调用。 异步操作完成后,一次性任务可以取消停放,然后重新插入到活动任务中,这些活动将连续调用以取得进展。 当然,要能够取消任务,系统的其他部分必须检测到异步操作已完成,以及要为该异步操作取消任务。
显然,如果在应用程序中只有一个线程正在执行,则不能利用多个CPU。 解决方案是启动多个线程。 通常,每个CPU一个线程-取决于您的线程需要执行哪种任务。 如果您有需要执行阻塞IO工作的任务,例如从文件系统或网络中读取数据,则每个CPU可能需要多个线程。 每个线程将在等待阻塞的IO操作完成时被阻塞,不执行任何操作。
当您将单线程体系结构扩展到多个单线程子系统时,从技术上讲,它不再是单线程的。 但是,每个单线程子系统通常都将被设计为一个单线程系统,并表现为一个单线程系统。 我曾经将这样的多线程单线程系统称为同线程系统,尽管我不确定这实际上是最精确的术语。 我们可能需要重新审视这些不同的设计,并在将来为它们提供更具描述性的术语。
译自: Singlethreaded Concurrency
Jakob Jenkov
Last update: 2020-12-11
如何用node.js模拟多线程
Fibers 又称纤程,可以理解为协同程序,类似py和lua都有这样的模型。使用Fibers可以避免对资源的互抢,减少cpu和内存的消耗,但是Fibers并不能够真正的并行执行,同一时刻只有一个Fibers在执行,如果在其中一个Fibers中执行过多的cpu操作或者写了个死循环,则整个主程序将卡死住。node中的异步事件循环模型就有点象这个。
Threads 又称线程,他可以在同一时刻并行的执行,他们共享主进程的内存,在其中某一时刻某一个threads锁死了,是不会影响主线程以及其他线程的执行。但是为了实现这个模型,我们不得不消耗更多的内存和cpu为线程切换的开销,同时也存在可能多个线程对同一内存单元进行读写而造成程序崩溃的问题。
很多让node支持多线程的方法是使用c/c++的addon来实现,在需要进行cpu密集型计算的地方,把js代码改写成c/c++代码,但是如果开发人员对c++不是很熟悉,一来开发效率会降低不少,二来也容易出bug,而且我们知道在addon中的c++代码除了编译出错外,是很难调试的,毕竟没有vs调试c++代码方便。
令人振奋的消息,我们为什么不让node也支持多线程模型呢?于是Jorge为我们开发出了一个让node支持多线程模型的模块:threads_a_gogo
***隐藏网址***
有了threads-a-gogo(以下简称TAGG)这个模块之后,我们可以让node做更多的事情,我记得以前我看过一篇文章,说node只能应付i/o密集型场景,在cpu密集型场景将完败给apache,因为apache是为每一个请求起一条线程的,所以在处理cpu密集型任务时一个线程的高强度计算不会很大程度的影响其他线程,类似的还有php的fastcgi,这也是很多拿node和php进行比较时,php的拥护者们一直提出的理论。
我们先来做一个简单的测试,用我们suqian大大最喜欢的斐波那契数组来看一下,加入了多线程的node有多么的强悍:(测试机器为4CPU)
没有使用TAGG的正常情况,异步也帮不了我们应对cpu密集型任务
function fibo (n) {
return n 》 1 ? fibo(n - 1) + fibo(n - 2) : 1;
}
var n=8
function back(){
if(!--n) return console.timeEnd(’no thread’);
}
console.time(’no thread’);
process.nextTick(function(){
console.log(fibo (40));
back();
})
process.nextTick(function(){
console.log(fibo (40));
back();
})
process.nextTick(function(){
console.log(fibo (40));
back();
})
process.nextTick(function(){
console.log(fibo (40));
back();
})
process.nextTick(function(){
console.log(fibo (40));
back();
})
process.nextTick(function(){
console.log(fibo (40));
back();
})
process.nextTick(function(){
console.log(fibo (40));
back();
})
process.nextTick(function(){
console.log(fibo (40));
back();
})
我们模拟了8个异步的行为,测试用的node v0.8.16版本,所以 process.nextTick还是异步方法。最后我们输出结果为:
165580141
165580141
165580141
165580141
165580141
165580141
165580141
165580141
no thread: 23346ms
接下来我们使用TAGG模块来测试同样的执行8次斐波那契数组计算,看看成绩如何?
function fibo (n) {
return n 》 1 ? fibo(n - 1) + fibo(n - 2) : 1;
}
console.time(’8 thread’);
var numThreads= 8; //创建线程池,最大数为8
var threadPool= require(’threads_a_gogo’).createPool(numThreads).all.eval(fibo); //为线程池注册程序
var i=8;
var cb = function(err,data){ //注册线程执行完毕的回调函数
console.log(data);
if(!--i){
threadPool.destroy();
console.timeEnd(’8 thread’);
}
}
threadPool.any.eval(’fibo(40)’, cb); //开始向线程池中执行fibo(40)这个任务
threadPool.any.eval(’fibo(40)’, cb);
threadPool.any.eval(’fibo(40)’, cb);
threadPool.any.eval(’fibo(40)’, cb);
threadPool.any.eval(’fibo(40)’, cb);
threadPool.any.eval(’fibo(40)’, cb);
threadPool.any.eval(’fibo(40)’, cb);
threadPool.any.eval(’fibo(40)’, cb);
最重的结果:
165580141
165580141
165580141
165580141
165580141
165580141
165580141
165580141
8 thread: 9510ms
相比不使用多线程模型的node,使用了TAGG模块之后,我们在4CPU服务器上的测试结果要快上一倍还不止。
到这里我们看上去找到了一个比较完美的解决方案应对CPU密集型任务,但是可能有同学会说,我可以使用cluster来做相同的事情,下面我们来做一个使用cluster计算这些任务的情况:
var cluster = require(’cluster’);
var numCPUs = 8;
function fibo (n) {
return n 》 1 ? fibo(n - 1) + fibo(n - 2) : 1;
}
console.time(’8 cluster’);
if (cluster.isMaster) {
// Fork workers.
for (var i = 0; i 《 numCPUs; i++) {
cluster.fork();
}
var i = 8;
cluster.on(’exit’, function(worker, code, signal) {
if(!--i){
console.timeEnd(’8 cluster’);
process.exit(0);
}
});
} else {
console.log(fibo (40));
process.exit(0);
}
***隐藏网址***
165580141
165580141
165580141
165580141
165580141
165580141
165580141
165580141
8 cluster: 11925ms
TAGG模块还有其他更多的功能,比如事件触发,平滑退出,查看线程工作状态等等,总之TAGG模块给node注入了新的活力,让node一直饱受诟病的处理cpu密集任务问题得到了一个妥善的解决,就算你不擅长c++代码,也能够轻松编写出多线程的真正的非阻塞node程序了。
js是单线程的,那么它怎么处理并发,难道要排队么
基于异步IO和事件,举个栗子:
有个点餐员是这样工作的,每次客人点了一个菜,他就站在那儿等菜做好了,然后端出去,所以老板不得不多请几个店员来完成工作。
但是另一个店员是这样工作了,每次客人点餐,他告诉后厨“西红柿炒鸡蛋做好了,送到一号餐桌”,然后他继续去点餐了,所以他可以不停地处理客人的点餐。
就是这个道理

更多文章:
全球新冠肺炎疫情背景下航运发展(盐田港复苏日志:半年历劫从“低谷”到“爆仓” 疫情之后巨轮如何越洋航行)
2026年9月7日 17:10
matlab求解带字母参数方程组(我想matlab求一个关于x,y的方程组 ab c d f e h m n 都是参数)
2026年9月7日 16:30
oracle中的循环语句(下面哪个不是oracle程序设计中的循环语句 a for)
2026年9月7日 15:30
电脑里2个系统怎么删除一个(电脑开机显示有两个系统,如何删除一个)
2026年9月7日 12:20
scrollthrough意思(“scroll”是什么意思)
2026年9月7日 08:00
怎么激活keygen(注册机如何激活cad2008一个简单激活cad2008的方法)
2026年9月7日 06:30



