NODEJS硬實戰(zhàn)筆記(多進(jìn)程)

利用NODE整合外部應(yīng)用程序

執(zhí)行外部應(yīng)用程序

  • execFile:執(zhí)行外部程序寂纪,并且需要提供一組參數(shù),以及一個在進(jìn)程退出后的緩沖輸出的回調(diào)统翩。
  • spawn:執(zhí)行外部程序锣杂,并且需要提供一組參數(shù),以及一個在進(jìn)程退出后的輸入輸出和時間的數(shù)據(jù)流接口畅厢。
  • exec:在一個命令行窗口中執(zhí)行一個或多個命令冯痢,以及一個在進(jìn)程退出后緩沖輸出的回調(diào)。
  • fork:在一個獨立的進(jìn)程中執(zhí)行一個Node模塊框杜,并且需要提供一組參數(shù)浦楣,以及一個類似spawn方法里的數(shù)據(jù)流和事件式的接口,同時設(shè)置好父進(jìn)程和子進(jìn)程之間的進(jìn)程間通信霸琴。

execFile

這是一個非常通用的方法椒振,運行一個外部程序并且得到相應(yīng)的輸出結(jié)果。該方法是一個異步方法梧乘,在外部應(yīng)用的輸出內(nèi)部使用buffer存放起來直到外部應(yīng)用退出時,回調(diào)被調(diào)用傳入對應(yīng)的輸出數(shù)據(jù)庐杨。

var cp = require('child_process')

cp.execFile('echo', ['hello', 'world'],
    function(err, stdout, stderr) {
        if (err) console.error(err);
        console.log('stdout', stdout);
        console.log('stderr', stderr);
    });

Windows/UNIX操作系統(tǒng)中都有一個PATH的環(huán)境變量选调,PATH包含了一組可執(zhí)行程序的執(zhí)行目錄列表。在Node中灵份,當(dāng)后臺運行execvp時仁堪,當(dāng)沒有提供絕對或者相對路徑時,它會基于PATH里定義的路徑搜索所有相關(guān)的程序填渠。

如果想要快速檢查PATH路徑包含哪一些目錄弦聂,可以在Node的交互式命令解析器里輸入:

$ node
> console.log(process.env.PATH.split(':').join('\n'))
/usr/local/bin
/usr/bin/bin
...

當(dāng)然你可以繼續(xù)向PATH路徑當(dāng)中添加你的路徑,但是必須保證這個設(shè)置是在你執(zhí)行execFile之前氛什。

process.env.PATH += ':/a/new/path/to/executable';

執(zhí)行外部程序時出現(xiàn)的異常

主要的異常分為兩種莺葫,一種是提供的路徑或文件名稱不存在,一種是提供的應(yīng)用路徑被鎖定(執(zhí)行應(yīng)用的權(quán)限不足)枪眉。

  • 提供的路徑或文件名不存在時:通常會報錯ENOENT
  • 執(zhí)行應(yīng)用的權(quán)限不足:通常會報錯EACCESS或者EPERM
  • 該程序不能在當(dāng)前的平臺下執(zhí)行時:外部程序退出返回的狀態(tài)碼非零(即err.code!=0)

spawn

對于調(diào)用一個你可能預(yù)期有大數(shù)據(jù)量輸出的外部應(yīng)用捺檬,流確實是一個很好的選擇。當(dāng)外部程序輸出的數(shù)據(jù)可用時贸铜,此時你可以選擇馬上將數(shù)據(jù)輸出到內(nèi)部應(yīng)用堡纬,通過流。相反蒿秦,而不是等到將所有數(shù)據(jù)緩存好之后再將其輸出烤镐。

var cp = require('child_process');

var child = cp.spawn('echo', ['hello', 'world']);
child.on('error', console.error);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);

實例化一個spawn后,會返回一個ChildProccess對象棍鳖,該對象中包含了stdin炮叶、stdout和stderr流對象,并且因為其流的性質(zhì),可以很好的進(jìn)行無縫的處理悴灵。

var cp = require('child_process');
var cat = cp.spawn('cat', ['messy.txt']);
var sort = cp.spawn('sort');
var uniq = cp.spawn('uniq');

cat.stdout.pipe(sort.stdin);
sort.stdout.pipe(uniq.stdin);
uniq.stdout.pipe(process.stdout);

exec

如果需要在命令解析器里執(zhí)行命令扛芽,你可以選擇使用exec。exec方法實際上也是調(diào)用/bin/sh(在UNIX/Linux下)或者cmd.exe(在Windows下)來執(zhí)行命令的积瞒。當(dāng)然川尖,這種方法可行的前提是,你必須擁有其他需要被執(zhí)行的命令的權(quán)限(如管道茫孔、重定向和后臺命令)叮喳。

cp.exec('cat messy.txt | sort | uniq', 
    function(err, stdout, stderr) {
        console.log(stdout);
    })

分離子進(jìn)程

  • 形式上的分離

    當(dāng)在進(jìn)程里開啟該進(jìn)程的子進(jìn)程以后,子進(jìn)程會依賴于父進(jìn)程缰贝。當(dāng)父進(jìn)程關(guān)閉的時候馍悟,子進(jìn)程也會隨著關(guān)閉,并且子進(jìn)程是沒有自己的獨立I/O剩晴。那么如果想讓子進(jìn)程脫離父進(jìn)程锣咒,就需要使用spawn方法,從而使得子進(jìn)程擁有和父進(jìn)程一樣的級別赞弥,即成為一個進(jìn)程組的頭毅整。

    var child = cp.spawn('./', [], {detached: true});
    

    但是此時,子進(jìn)程和父進(jìn)程之間還是通過I/O互相連接的绽左,所以如果不強(qiáng)制性終結(jié)正在運行的Node程序悼嫉,就會發(fā)現(xiàn)父進(jìn)程會一直保持活躍狀態(tài),直到子進(jìn)程結(jié)束拼窥。但是強(qiáng)制性終結(jié)Node程序以后戏蔑,longrun會繼續(xù)執(zhí)行,直到它自己終結(jié)鲁纠。

  • I/O分離

    而stdio選項就是來控制子進(jìn)程的I/O連接到一個具體的地方总棵,
    stdio:['pipe', 'pipe', 'pipe']三個流分別對應(yīng)child.stdinchild.stdoutchild.stderr房交。默認(rèn)這些流都是開放的彻舰,所以父進(jìn)程能夠與子進(jìn)程之間進(jìn)行通信。當(dāng)然你可以使用很暴力的方式關(guān)閉掉這些流候味,阻止父進(jìn)程與子進(jìn)程進(jìn)行通信刃唤。

    child.stdin.destroy();
    child.stdout.destroy();
    child.stderr.destroy();
    

    但是既然我們根本不需要這些流,那就應(yīng)該在源頭上去放棄掉這些流或者重新賦值將I/O指向別的地方白群。

    var fs = require('fs');
    var cp = require('child_process');
    
    var outFd = fs.openSync('./longrun.out', 'a');
    var errFd = fs.openSync('./longrun.err', 'a');
    
    var child = cp.spawn('./longrun', [], {
        detached: true,
        stdio: ['ignore', outFd, errFd]
    });
    

    ignore關(guān)鍵詞就是用來放棄相對應(yīng)的流尚胞。

  • 引用分離

    盡管子進(jìn)程被分離了并且它和父進(jìn)程的I/O也被中斷了,但是父進(jìn)程仍然會有一個堆子進(jìn)程的內(nèi)部引用帜慢,并且只要子進(jìn)程沒有終結(jié)且這個引用沒有被移除笼裳,父進(jìn)程都不會終結(jié)唯卖。所以可以通過child.unref()方法告訴Node不要將子進(jìn)程的引用進(jìn)行計數(shù)。下面的代碼就會再子進(jìn)程執(zhí)行spawn方法之后退出躬柬。

    var fs = require('fs');
    var cp = require('child_process');
    
    var outFd = fs.openSync('./longrun.out', 'a');
    var errFd = fs.openSync('./longrun.err', 'a');
    
    var child = cp.spawn('./longrun', [], {
        detached: true,
        stdio: ['ignore', outFd, errFd]
    });
    
    child.unref();
    

fork(操作一個獨立的Node進(jìn)程)

var cp = require('child_process');
var child = cp.fork('./myChild');

默認(rèn)情況下拜轨,通過fork創(chuàng)建的子進(jìn)程所有的輸入輸出都是繼承自父進(jìn)程的,并不會有child.stdin允青、child.stdoutchild.stderr橄碾。

如果想提供像spawn一樣的默認(rèn)的I/O配置,那么可以使用slient選項颠锉。

var cp = require('child_process');
var child = cp.fork('./myChild', { silent: true });

使用fork方法會開放一個IPC通道法牲,使得不同的Node進(jìn)程之間進(jìn)行消息傳送。而Node進(jìn)程之間主要是使用Event進(jìn)行通信琼掠,在子進(jìn)程這邊會暴露process.on('message')process.send()來接收和發(fā)送消息拒垃,在父進(jìn)程這邊使用child.on('message')child.send()

var cp = require('child_process');
var child = cp.fork('./child');
child.on('message', function(msg) {
    console.log('got a message from child', msg);
});
child.send('sending a string');

因為我們打開了一個父進(jìn)程和子進(jìn)程間的一個IPC通道瓷蛙,只要子進(jìn)程不中斷悼瓮,父進(jìn)程也就會保持活動狀態(tài)。如果需要中斷IPC通信連接速挑,可以在父進(jìn)程中顯式的實現(xiàn):child.disconnect();

一個較好的父進(jìn)程例子谤牡,考慮了多次調(diào)用以及子進(jìn)程出現(xiàn)問題的情況:

function doWork(job, cb) {
    var child = cp.fork('./worker');
    var cbTriggered = false;
    
    child
        .once('error', function(err) {
            if (!cbTriggered) {
                cb(err);
                cbTriggered = true;
            }
            // 子進(jìn)程出現(xiàn)了異常則殺死子進(jìn)程
            child.kill();
        })
        .once('exit', function(code, signal) {
            if (!cbTriggered)
                cb(new Error('Child exited with code: ' + code));
        })
        .once('message', function(result) {
            cb(null, result);
            cbTriggered = true;
        })
        .send(job);
}

工作池

在Node的官方文檔中有述:

這些子節(jié)點仍然是一個V8的新實例。預(yù)計每一個節(jié)點需要耗時30毫秒的啟動時間和10MB的內(nèi)存姥宝。
也就是說,你不能創(chuàng)建太多了恐疲,因為這些并不是沒有代價開銷的腊满。

所以說在實現(xiàn)的過程當(dāng)中,與其使用多個短時間的子進(jìn)程培己,還不如維護(hù)一個工作池碳蛋,池中存放了一些可以長時間運行的進(jìn)程。

那么我們就在上面doWork的基礎(chǔ)之上省咨,做一些優(yōu)化肃弟,完成我們在工作池上的一個作業(yè)分配以及發(fā)送。

var cp = require('child_process');
var cpus = require('os').cpus().length;

module.exports = function(workModule) {
    // 等待的作業(yè)
    var awaiting = [];
    // 空閑的子進(jìn)程
    var readyPool = [];
    // 總子進(jìn)程的個數(shù)
    var poolSize = 0;
    
    return function doWork(job, cb) {
        // 如果現(xiàn)在沒有準(zhǔn)備好的子進(jìn)程零蓉,并且總子進(jìn)程數(shù)已經(jīng)超過cpu的個數(shù)了笤受,就讓作業(yè)先排隊等待
        if (!readyPool.length && poolSize > cpus) 
            return awaiting.push([ doWork, job, cb ]);
        
        // 如果有空閑的子進(jìn)程則取出第一個使用,沒有的話就新建一個子進(jìn)程
        var child = readyPool.length
            ? readyPool.shift()
            : (poolSize++, cp.fork(workModule));
        var cbTriggered = false;
        
        child
            // 先刪除原來的監(jiān)聽
            .removeAllListeners()
            .once('error', function(err) {
                if (!cbTriggered) {
                    cb(err);
                    cbTriggered = true;
                }
                child.kill();
            })
            .once('exit', function() {
                if (!cbTriggered)
                    cb(new Error('Child exited with code: ' + code));
                // 進(jìn)程關(guān)閉的時候?qū)⑺鼜年犃兄刑叱?                poolSize--;
                var childIdx = readyPool.indexOf(child);
                if (childIdx > -1) readyPool.splice(childIdx, 1); 
            })
            .once('message', function(msg) {
                cb(null, msg);
                cbTriggered = true;
                // 子進(jìn)程再次就緒敌蜂,將其加回readPool
                readyPool.push(child);
                // 如果現(xiàn)在有等待的任務(wù)箩兽,運行之
                if (awaiting.length) setImmediate.apply(null, awaiting.shift());
            })
    }
}

在這段代碼的最后我是有一些疑問的,因為當(dāng)子進(jìn)程收到message的時候章喉,進(jìn)行了異步回調(diào)汗贫。因為當(dāng)時沒太能分清異步與多線程的區(qū)別身坐。所以我當(dāng)時就不太懂回調(diào)都還沒有結(jié)束,為什么就能回收這個進(jìn)程了落包?

那么這邊也區(qū)分一下異步與多線程部蛇。

  • 異步:首先說一下DMA,DMA就是直接內(nèi)存訪問的意思咐蝇,也就是說涯鲁,擁有DMA功能的硬件在和內(nèi)存進(jìn)行數(shù)據(jù)交換的時候可以不消耗CPU資源。只要CPU在發(fā)起數(shù)據(jù)傳輸時發(fā)送一個指令嘹害,硬件就開始自己和內(nèi)存交換數(shù)據(jù)撮竿,在傳輸完成之后硬件會觸發(fā)一個中斷來通知操作完成。這些無須消耗CPU時間的I/O操作正是異步操作的硬件基礎(chǔ)笔呀。所以即使在DOS這樣的單進(jìn)程(而且無線程概念)系統(tǒng)中也同樣可以發(fā)起異步的DMA操作幢踏。因為異步操作無須額外的線程負(fù)擔(dān),并且使用回調(diào)的方式進(jìn)行處理许师,在設(shè)計良好的情況下房蝉,處理函數(shù)可以不必使用共享變量(即使無法完全不用,最起碼可以減少 共享變量的數(shù)量)微渠,減少了死鎖的可能搭幻。當(dāng)然異步操作也并非完美無暇。編寫異步操作的復(fù)雜程度較高逞盆,程序主要使用回調(diào)方式進(jìn)行處理檀蹋,與普通人的思維方式有些出入,而且難以調(diào)試云芦。
  • 多線程:線程不是一個計算機(jī)硬件的功能俯逾,而是操作系統(tǒng)提供的一種邏輯功能,線程本質(zhì)上是進(jìn)程中一段并發(fā)運行的代碼舅逸,所以線程需要操作系統(tǒng)投入CPU資源來運行和調(diào)度桌肴。多線程的優(yōu)點很明顯,線程中的處理程序依然是順序執(zhí)行琉历,符合普通人的思維習(xí)慣坠七,所以編程簡單。但是多線程的缺點也同樣明顯旗笔,線程的使用(濫用)會給系統(tǒng)帶來上下文切換的額外負(fù)擔(dān)彪置。并且線程間的共享變量可能造成死鎖的出現(xiàn)。

詳細(xì)可見:淺談多線程和異步

同步運行

其實在異步的API介紹完以后换团,同步的API就顯得很簡單了悉稠。因為它和異步的API基本上是一樣的,只不過在實現(xiàn)的過程當(dāng)中會阻塞掉主線程艘包,直到子進(jìn)程模塊完成的猛。

  • 如果只想同步執(zhí)行一個單獨的命令耀盗,并且得到輸出,那么可以使用execFileSync

    var ex = require('child_process').execFileSync;
    var stdout = ex('echo', ['hello']).toString();
    console.log(stdout);
    
  • 如果想程序式同步執(zhí)行多個命令卦尊,并且命令之間的結(jié)果存在相互依賴的關(guān)系叛拷,可以使用spawnSync

    var sp = require('child_process').spawnSync;
    var ps = sp('ps', ['aux']);
    var grep = sp('grep', ['node']) {
        input: ps.stdout;
        encoding: 'utf-8'
    });
    console.log(grep);
    

    同步子進(jìn)程得到的結(jié)果包含了很多的細(xì)節(jié),這也是使用spawnSync的另外一個好處岂却。

  • 當(dāng)然execSync也是同樣的使用方法忿薇,這里不再贅述

同步子進(jìn)程中的異常處理

如果在execSync或execFileSync執(zhí)行的結(jié)果中返回的是一個非零狀態(tài),這種情況下躏哩,將會有異常拋出署浩。這個拋出的異常對象將會包含我們在使用spawnExec返回的結(jié)果里的所有東西。我們可以訪問狀態(tài)編碼里的重要信息stderr流扫尺。

var ex = require('child-process').execFileSync;
try {
    ex('cd', ['non-existent-dir'], {
        encoding: 'utf-8'
    });
} catch(err) {
    console.error('exit status was', err.status);
    console.error('stderr', err.stderr);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末筋栋,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子正驻,更是在濱河造成了極大的恐慌弊攘,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姑曙,死亡現(xiàn)場離奇詭異襟交,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)伤靠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門捣域,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人宴合,你說我怎么就攤上這事竟宋。” “怎么了形纺?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長徒欣。 經(jīng)常有香客問我逐样,道長,這世上最難降的妖魔是什么打肝? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任脂新,我火速辦了婚禮,結(jié)果婚禮上粗梭,老公的妹妹穿的比我還像新娘争便。我一直安慰自己,他們只是感情好断医,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布滞乙。 她就那樣靜靜地躺著奏纪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪斩启。 梳的紋絲不亂的頭發(fā)上序调,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機(jī)與錄音兔簇,去河邊找鬼发绢。 笑死,一個胖子當(dāng)著我的面吹牛垄琐,可吹牛的內(nèi)容都是我干的边酒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼狸窘,長吁一口氣:“原來是場噩夢啊……” “哼墩朦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起朦前,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤介杆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后韭寸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體春哨,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年恩伺,在試婚紗的時候發(fā)現(xiàn)自己被綠了赴背。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡晶渠,死狀恐怖凰荚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情褒脯,我是刑警寧澤便瑟,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站番川,受9級特大地震影響到涂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜颁督,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一践啄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沉御,春花似錦屿讽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烂完。三九已至,卻和暖如春衩婚,著一層夾襖步出監(jiān)牢的瞬間窜护,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工非春, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留柱徙,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓奇昙,卻偏偏與公主長得像护侮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子储耐,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容