JavaScript中的进程、线程和协程
IvanGallard
8年前
<p>这周一直在编前端构建的脚本,用到了多进程去解决一个效率问题。期间差了很多进程、线程、协程的资料,在这里记录回顾一下。</p> <h2>概念</h2> <p>关于进程、线程、协程的概念这里就不再赘述了,具体的可以参考wiki百科:</p> <ul> <li> <p><a href="/misc/goto?guid=4959732639524522936" rel="nofollow,noindex">进程</a></p> </li> <li> <p><a href="/misc/goto?guid=4959732639611252861" rel="nofollow,noindex">线程</a></p> </li> <li> <p><a href="/misc/goto?guid=4959732639687795369" rel="nofollow,noindex">协程</a></p> </li> </ul> <h2>简单概括一下它们间的区别</h2> <p>就是相对线程和协程,进程更独立,有自己的内存空间,所以进程间通信比较困难。线程比进程轻量级,属于同一进程的多个线程间可以共享全部资源。协程与线程类似,不同点在于,线程由系统控制切换,协程是由用户控制切换。</p> <p>那么,控制切换,指的是控制什么的切换呢?</p> <p>在一个进程中执行的程序,有时需要同时处理多个工作,这时我们可以创建多个线程,让每个线程处理一个工作。但是,进程只有一个。就好比一个人,你给他分配了多个工作,帮他把每个工作单独拉了一个列表,可还是他一个人干,他只能一会儿干干这一会儿干干那,来模拟多个工作同时进行的状态,这就是所谓的系统控制切换,系统不停的在多个线程间切换来达到并行的效果。你可能会说,那根一件一件干不是一样吗?没错,是一样的,在只有一个cpu的电脑上,用不用多线程程序执行的时间是一样的。但是,如果这个人长了两个脑袋呢?那么他就能同时处理两件工作了。多核cpu就是那个长了好多个脑地的人……而协程的切换是要由用户手动来控制的,所以协程并适合并行计算,而更多的用来优化程序结构。</p> <h2>js都支持吗?</h2> <p>这要看js在什么环境运行。</p> <p>在浏览器中,可以通过webworkers创建进程,可以通过async/await,yield/Generator/GeneratorFunction实现协程,控制程序切换。</p> <p>在node中,除了可以使用上面浏览器中可以使用的方法,还可以通过cluster,child_process创建进程,通过libuv,tagg创建线程</p> <h2>刚才提到的那些都是啥?怎么用?</h2> <h2>webworkers</h2> <p>简单点儿说就是使用webworkers你可以在全新的环境中运行一个你指定的js文件。这个全新的环境是独立的,既一个全新的进程,有点儿像一个新iframe还没有window.top,window.parent属性,哈哈……</p> <p>webworkers创建的进程和主进程之间可以通过message事件传递消息,但是消息只能是字符串,所以想要传对象和数组就只能传json了……这也是他不方便的地方。</p> <p>具体使用方法可以看MDN上的文章: <a href="/misc/goto?guid=4959732639773170024" rel="nofollow,noindex">使用 Web Workers</a></p> <h2>async/await</h2> <p>async/await是es7中新加的两个关键字,async 可以声明一个异步函数,此函数需要返回一个 Promise 对象。await 可以等待一个 Promise 对象 resolve,并拿到结果。</p> <p>其实就是类似汇编的寄存器和跳转指令……呃,通俗的说就是可以根据状态跳转态另一个函数半中间。</p> <p>由于es7还未在各个环境实现,想要使用的话还的用一些babel-polyfill之类的库做兼容……</p> <p>更详细介绍请看阿阮的文章: <a href="http://es6.ruanyifeng.com/?search=async&x=0&y=0#docs/async" rel="nofollow,noindex">异步操作和Async函数</a></p> <h2>yield/Generator/GeneratorFunction</h2> <p>generator是es6中新增的函数,本质是可以将一个函数执行暂停,并保存上下文,再次调用时恢复当时的状态。但是用来解决协程切换的问题貌似有点儿滥用特性的感觉呢……</p> <p>更详细介绍请看阿阮的文章: <a href="http://es6.ruanyifeng.com/?search=async&x=0&y=0#docs/generator" rel="nofollow,noindex">Generator 函数</a></p> <h2>cluster</h2> <p>cluster是node官方提供的一个多进程模块,效果和C语言的fork函数类似,当前文件完全重新执行一遍,通过cluster.isMaster判断是不是主进程,在区分不同的操作。进程间通过事件回调来通信,NodeJS 0.6.x 以上的版本开始支持。</p> <p>示例代码就不放了,node官方文档上写的很详细: <a href="/misc/goto?guid=4959646474498485566" rel="nofollow,noindex">cluster</a></p> <h2>child_process</h2> <p>node自带的child_process模块里的fork函数可以实现类似浏览器里webworkers的效果,使用方法和webworker一毛一样,都是通过读取新文件开启新进程,通过message通信。</p> <p>具体介绍请看文档: <a href="/misc/goto?guid=4959732640049274960" rel="nofollow,noindex">child_process.fork(modulePath[, args][, options])</a></p> <p>官方文档没有示例,下面给出一个web服务接收参数计算斐波那契数组的例子:</p> <h3>index.js</h3> <pre> <code class="language-javascript">var express = require('express'); var fork = require('child_process').fork; var app = express(); app.get('/', function(req, res){ var worker = fork('./work_fibo.js') //创建一个工作进程 worker.on('message', function(m){//接收工作进程计算结果 if('object' === typeof m && m.type === 'fibo'){ worker.kill();//发送杀死进程的信号 res.send(m.result.toString());//将结果返回客户端 } }); worker.send({type:'fibo',num:~~req.query.n || 1});//发送给工作进程计算fibo的数量 }); app.listen(8124); </code></pre> <h3>work_fibo.js</h3> <pre> <code class="language-javascript">var fibo = functionfibo(n){//定义算法 return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1; } process.on('message', function(m){ //接收主进程发送过来的消息 if(typeof m === 'object' && m.type === 'fibo'){ var num = fibo(~~m.num); //计算jibo process.send({type: 'fibo',result:num}) //计算完毕返回结果 } }); process.on('SIGHUP', function(){ process.exit();//收到kill信息,进程退出 }); </code></pre> <h2>libuv</h2> <p>libuv是node底层实现使用的c++库……呃,所以如果你想使用这个库来实现多线程,那么你就得编写c++的代码了,不得不说,要想真正理解程序的本质,不多掌握几门语言真是不行啊……</p> <p>对c++不了解我就不瞎BB了,推荐两篇文章延伸阅读:</p> <ul> <li><a href="/misc/goto?guid=4959732640137785010" rel="nofollow,noindex">libuv多线程处理的简单示例</a></li> <li><a href="/misc/goto?guid=4959732640220648698" rel="nofollow,noindex">利用libuv编写异步多线程的addon实例</a></li> </ul> <h2>tagg</h2> <p>tagg(Threads a gogo for Node.js)是Jorge Chamorro Bieling开发的一个node包。使用c语言phread库实现的多线程。</p> <p>还是那刚才的斐波那契数组计算为例:</p> <pre> <code class="language-javascript">var Threads = require('threads_a_gogo');//加载tagg包 functionfibo(n){//定义斐波那契数组计算函数 return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1; } var t = Threads.create().eval(fibo); t.eval('fibo(35)', function(err, result){//将fibo(35)丢入子线程运行 if (err) throw err; //线程创建失败 console.log('fibo(35)=' + result);//打印fibo执行35次的结果 }); console.log('not block');//打印信息了,表示没有阻塞 </code></pre> <p>最后结果:</p> <pre> <code class="language-javascript">not block fibo(35)=14930352 </code></pre> <p>我们可以看到执行效果与webworker类似,不同的是通信使用了异步回调的方式。</p> <p>值得一提的是tagg包目前只能在linux下安装运行,这里再推荐一个tagg2包,是跨平台的。</p> <p>这里需要重点提一下的是,不论tagg还是tagg2包都是利用phtread库和v8的v8::Isolate Class类来实现js多线程功能的。</p> <p>Isolate代表着一个独立的v8引擎实例,v8的Isolate拥有完全分开的状态,在一个Isolate实例中的对象不能够在另外一个Isolate实例中使用。嵌入式开发者可以在其他线程创建一些额外的Isolate实例并行运行。在任何时刻,一个Isolate实例只能够被一个线程进行访问,可以利用加锁/解锁进行同步操作。</p> <p>换而言之,我们在进行v8的嵌入式开发时,无法在多线程中访问js变量,这条规则将直接导致我们之前的tagg2里面线程执行的函数无法使用Node.js的核心api,比如fs,crypto等模块。</p> <p>延伸阅读:</p> <ul> <li><a href="/misc/goto?guid=4959732640301480659" rel="nofollow,noindex">tagg</a></li> <li><a href="/misc/goto?guid=4959732640383245706" rel="nofollow,noindex">tagg2</a></li> </ul> <h2>总结</h2> <p>经过以上的学习,我们大概应该了解到进程、线程、协程的使用场景了,进程、线程适合用来处理计算密集型操作,协程适合用来优化代码结构,解决回调函数嵌套问题。线程比进程更轻,更节省资源,但是由于上面提到的线程问题,针对一些可以使用js原生的大量计算或循环还可以用用,涉及到使用nodejs核心api的操作,就要用进程解决了。</p> <h2>p.s. 我的问题</h2> <p>我在工作中使用的是fis配合grunt调用打包。由于要同时打包多个项目,grunt和fis都会定义全局变量,各个模块之间的配置可能会相互影响,各个模块在打包过程中又没有相互的通信,同时为了提高效率,非常时候适合使用多进程的方式来运行脚本。所以用cluster实现了多进程打包的。</p> <p>最后,祝大家新年快乐,1号胖三斤,3号金三胖~ ┑( ̄Д  ̄)┍</p> <p> </p> <p>来自:http://brooch.me/2016/12/30/process-thread-and-coroutine-in-javascript/</p> <p> </p>