js的单线程和多进程

概述

现行的软件架构主要有两种:单进程多线程(如:memcached、redis、mongodb等)和多进程单线程(nginx、node)。
单进程多线程的主要特点:

  • 快:线程比进程轻量,它的切换开销要少很多。进程相当于函数间切换,每个函数拥有自己的变量;线程相当于一个函数内的子函数切换,它们拥有相同的全局变量。
  • 灵活: 程序逻辑和控制方式简单,但是锁和全局变量同步比较麻烦。
  • 稳定性不高: 由于只有一个进程,其内部任何线程出现问题都有可能造成进程挂掉,造成不可用。
  • 性能天花板:线程和主程序受限2G地址空间;当线程到一定数量后,即使增加cpu也不能提升性能。

多进程单线程的主要特点:

  • 高性能:没有频繁创建和切换线程的开销,可以在高并发的情况下保持低内存占用;可以根据CPU的数量增加进程数。
  • 线程安全:没有必要对变量进行加锁解锁的操作
  • 异步非阻塞:通过异步I/O可以让cpu在I/O等待的时间内去执行其他操作,实现程序运行的非阻塞
  • 性能天花板:进程间的调度开销大、控制复杂;如果需要跨进程通信,传输数据不能太大。

事实上异步通过信号量、消息等方式早就存在操作系统底层,但是一直没有能在高级语言中推广使用。
Linux Unix提供了epoll方便了高级语言的异步设计。epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll只会把哪个流发生了怎样的I/O事件通知我们;libevent和libev都是对epoll的封装,nginx自己实现了对epoll的封装。

浏览器

在支持html5的浏览器里,可以使用webworker来将一些耗时的计算丢入worker进程中执行,这样主进程就不会阻塞,用户也就不会有卡顿的感觉了。

<!DOCTYPE html>
    <head>
        <title>worker</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <script>
            function init(){
                //创建一个Worker对象,并向它传递将在新进程中执行的脚本url
                var worker = new Worker('./webworker.js');
                //接收worker传递过来的数据
                worker.onmessage = function(event){
                    document.getElementById('result').innerHTML+=event.data+"<br/>" ;
                };
            };
        </script>
    </head>
    <body onload = "init()">
        <div id="result"></div>
    </body>
</html>

// webworker.js
var i = 0;
function timedCount(){
    for(var j = 0, sum = 0; j < 100; j++){
        for(var i = 0; i < 100000000; i++){
            sum+=i;
        };
    };
    //将得到的sum发送回主进程
    postMessage(sum);
};
//将执行timedCount前的时间,通过postMessage发送回主进程
postMessage('Before computing, '+new Date());
timedCount();
//结束timedCount后,将结束时间发送回主进程
postMessage('After computing, ' +new Date());

Node

Nodejs通过其内置的cluster模块实现多进程。cluster是对child_process进行了封装,目的是发挥多核服务器的性能;pm2 是当下最热门的带有负载均衡功能的 Node.js 应用进程管理器。实际开发时,我们不需要关注多进程环境。

进程模型

Node的多进程模型是一个主master多个从worker模式,master的职责如下:

  • 接收外界信号并向各worker进程发送信号
  • 监控woker进程的运行状态,当woker进程退出后(异常情况下),会自动重新启动新的woker进程(进程守护)。
盗用图片..

如果只是简单的fork几个进程,多个进程之间会竞争 accpet 一个连接,产生惊群现象,效率比较低。同时由于无法控制一个新的连接由哪个进程来处理,必然导致各 worker 进程之间的负载非常不均衡。

IPC

注: 这部分是从当我们谈论 cluster 时我们在谈论什么(下)copy而来

Node.js 中父进程调用 fork 产生子进程时,会事先构造一个 pipe 用于进程通信。

  new process.binding('pipe_wrap').Pipe(true);

构造出的 pipe 最初还是关闭的状态,或者说底层还并没有创建一个真实的 pipe,直至调用到 libuv 底层的uv_spawn, 利用 socketpair 创建的全双工通信管道绑定到最初 Node.js 层创建的 pipe 上。
管道此时已经真实的存在了,父进程保留对一端的操作,通过环境变量将管道的另一端文件描述符 fd 传递到子进程。

  options.envPairs.push('NODE_CHANNEL_FD=' + ipcFd);

子进程启动后通过环境变量拿到 fd

  var fd = parseInt(process.env.NODE_CHANNEL_FD, 10);

并将 fd 绑定到一个新构造的 pipe 上

  var p = new Pipe(true);
  p.open(fd);

于是父子进程间用于双向通信的所有基础设施都已经准备好了。

总结下,Nodejs通过pipe实现IPC,主要包括以下几个步骤:

  • 主进程在fork产生子进程前生成一个pipe占位符,提示后续会有pipe创建。
  • 通过系统的socketpair把双工通道绑定到此pipe占位符上。
  • 通过环境变量把文件描述符fd传给子进程。
  • 子进程通过fd创建pipe,此pipe替代占位符进行通信。

例子:

// master
const WriteWrap = process.binding('stream_wrap').WriteWrap;
var cp = require('child_process');

var worker = cp.fork(__dirname + '/ipc_worker.js');
var channel = worker._channel;

channel.onread = function (len, buf, handle) {
    if (buf) {
        console.log(buf.toString())
        channel.close()
    } else {
        channel.close()
        console.log('channel closed');
    }
}

var message = { hello: 'worker',  pid: process.pid };
var req = new WriteWrap();
var string = JSON.stringify(message) + '\n';
channel.writeUtf8String(req, string, null);

// worker
const WriteWrap = process.binding('stream_wrap').WriteWrap;
const channel = process._channel;

channel.ref();
channel.onread = function (len, buf, handle) {
    if (buf) {
        console.log(buf.toString())
    }else{
        process._channel.close()
        console.log('channel closed');
    }
}

var message = { hello: 'master',  pid: process.pid };
var req = new WriteWrap();
var string = JSON.stringify(message) + '\n';
channel.writeUtf8String(req, string, null);
进程失联

进程失联是在子进程退出前通知主进程,主进程fork一个新的子进程,然后原来的子进程退出;主进程通过是子进程的disconnect事件监听其状态。
例子:

const WriteWrap = process.binding('stream_wrap').WriteWrap;
const net = require('net');
const fork = require('child_process').fork;

var workers = [];
for (var i = 0; i < 4; i++) {
     var worker = fork(__dirname + '/multi_worker.js');
     worker.on('disconnect', function () {
         console.log('[%s] worker %s is disconnected', process.pid, worker.pid);
     });
     workers.push(worker);
}

var handle = net._createServerHandle('0.0.0.0', 3000);
handle.listen();
handle.onconnection = function (err,handle) {
    var worker = workers.pop();
    var channel = worker._channel;
    var req = new WriteWrap();
    channel.writeUtf8String(req, 'dispatch handle', handle);
    workers.unshift(worker);
}

const net = require('net');
const WriteWrap = process.binding('stream_wrap').WriteWrap;
const channel = process._channel;
var buf = 'hello Node.js';
var res = ['HTTP/1.1 200 OK','content-length:' + buf.length].join('\r\n') + '\r\n\r\n' + buf;

channel.ref(); //防止进程退出
channel.onread = function (len, buf, handle) {
    console.log('[%s] worker %s got a connection', process.pid, process.pid);
    var socket = new net.Socket({
        handle: handle
    });
    socket.readable = socket.writable = true;
    socket.end(res);
    console.log('[%s] worker %s is going to disconnect', process.pid, process.pid);
    channel.close();
}
参考文章

当我们谈论 cluster 时我们在谈论什么(上)
当我们谈论 cluster 时我们在谈论什么(下)

多进程单线程模型与单进程多线程模型之争
多进程和多线程的优缺点
Node.js的线程和进程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 1. Nginx的模块与工作原理 Nginx由内核和模块组成,其中,内核的设计非常微小和简洁,完成的工作也非常简单...
    rosekissyou阅读 10,184评论 5 124
  • 第一章 Nginx简介 Nginx是什么 没有听过Nginx?那么一定听过它的“同行”Apache吧!Ngi...
    JokerW阅读 32,635评论 24 1,002
  • # 模块机制 node采用模块化结构,按照CommonJS规范定义和使用模块,模块与文件是一一对应关系,即加载一个...
    RichRand阅读 2,482评论 0 3
  • https://nodejs.org/api/documentation.html 工具模块 Assert 测试 ...
    KeKeMars阅读 6,297评论 0 6
  • NodeJS是单进程单线程 [1] 结构,适合编写IO密集型的网络应用。为了充分利用多核CPU的计算能力,最直接的...
    Yonny阅读 3,572评论 0 7