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进程中执行,这样主进程就不会阻塞,用户也就不会有卡顿的感觉了。


    
        worker
        
        
    
    
        
// 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进程(进程守护)。
js的单线程和多进程_第1张图片
盗用图片..

如果只是简单的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的线程和进程

你可能感兴趣的:(js的单线程和多进程)