NODEJS硬實戰筆記(多進程)

利用NODE整合外部應用程序

執行外部應用程序

  • execFile:執行外部程序,并且需要提供一組參數,以及一個在進程退出后的緩沖輸出的回調。
  • spawn:執行外部程序,并且需要提供一組參數,以及一個在進程退出后的輸入輸出和時間的數據流接口。
  • exec:在一個命令行窗口中執行一個或多個命令,以及一個在進程退出后緩沖輸出的回調。
  • fork:在一個獨立的進程中執行一個Node模塊,并且需要提供一組參數,以及一個類似spawn方法里的數據流和事件式的接口,同時設置好父進程和子進程之間的進程間通信。

execFile

這是一個非常通用的方法,運行一個外部程序并且得到相應的輸出結果。該方法是一個異步方法,在外部應用的輸出內部使用buffer存放起來直到外部應用退出時,回調被調用傳入對應的輸出數據。

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操作系統中都有一個PATH的環境變量,PATH包含了一組可執行程序的執行目錄列表。在Node中,當后臺運行execvp時,當沒有提供絕對或者相對路徑時,它會基于PATH里定義的路徑搜索所有相關的程序。

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

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

當然你可以繼續向PATH路徑當中添加你的路徑,但是必須保證這個設置是在你執行execFile之前。

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

執行外部程序時出現的異常

主要的異常分為兩種,一種是提供的路徑或文件名稱不存在,一種是提供的應用路徑被鎖定(執行應用的權限不足)

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

spawn

對于調用一個你可能預期有大數據量輸出的外部應用,流確實是一個很好的選擇。當外部程序輸出的數據可用時,此時你可以選擇馬上將數據輸出到內部應用,通過流。相反,而不是等到將所有數據緩存好之后再將其輸出。

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流對象,并且因為其流的性質,可以很好的進行無縫的處理。

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

如果需要在命令解析器里執行命令,你可以選擇使用exec。exec方法實際上也是調用/bin/sh(在UNIX/Linux下)或者cmd.exe(在Windows下)來執行命令的。當然,這種方法可行的前提是,你必須擁有其他需要被執行的命令的權限(如管道、重定向和后臺命令)。

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

分離子進程

  • 形式上的分離

    當在進程里開啟該進程的子進程以后,子進程會依賴于父進程。當父進程關閉的時候,子進程也會隨著關閉,并且子進程是沒有自己的獨立I/O。那么如果想讓子進程脫離父進程,就需要使用spawn方法,從而使得子進程擁有和父進程一樣的級別,即成為一個進程組的頭。

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

    但是此時,子進程和父進程之間還是通過I/O互相連接的,所以如果不強制性終結正在運行的Node程序,就會發現父進程會一直保持活躍狀態,直到子進程結束。但是強制性終結Node程序以后,longrun會繼續執行,直到它自己終結。

  • I/O分離

    而stdio選項就是來控制子進程的I/O連接到一個具體的地方,
    stdio:['pipe', 'pipe', 'pipe']三個流分別對應child.stdinchild.stdoutchild.stderr。默認這些流都是開放的,所以父進程能夠與子進程之間進行通信。當然你可以使用很暴力的方式關閉掉這些流,阻止父進程與子進程進行通信。

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

    但是既然我們根本不需要這些流,那就應該在源頭上去放棄掉這些流或者重新賦值將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關鍵詞就是用來放棄相對應的流

  • 引用分離

    盡管子進程被分離了并且它和父進程的I/O也被中斷了,但是父進程仍然會有一個堆子進程的內部引用,并且只要子進程沒有終結且這個引用沒有被移除,父進程都不會終結。所以可以通過child.unref()方法告訴Node不要將子進程的引用進行計數。下面的代碼就會再子進程執行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進程)

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

默認情況下,通過fork創建的子進程所有的輸入輸出都是繼承自父進程的,并不會有child.stdinchild.stdoutchild.stderr

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

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

使用fork方法會開放一個IPC通道,使得不同的Node進程之間進行消息傳送。而Node進程之間主要是使用Event進行通信,在子進程這邊會暴露process.on('message')process.send()來接收和發送消息,在父進程這邊使用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');

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

一個較好的父進程例子,考慮了多次調用以及子進程出現問題的情況:

function doWork(job, cb) {
    var child = cp.fork('./worker');
    var cbTriggered = false;
    
    child
        .once('error', function(err) {
            if (!cbTriggered) {
                cb(err);
                cbTriggered = true;
            }
            // 子進程出現了異常則殺死子進程
            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的官方文檔中有述:

這些子節點仍然是一個V8的新實例。預計每一個節點需要耗時30毫秒的啟動時間和10MB的內存。
也就是說,你不能創建太多了,因為這些并不是沒有代價開銷的。

所以說在實現的過程當中,與其使用多個短時間的子進程,還不如維護一個工作池,池中存放了一些可以長時間運行的進程。

那么我們就在上面doWork的基礎之上,做一些優化,完成我們在工作池上的一個作業分配以及發送。

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

module.exports = function(workModule) {
    // 等待的作業
    var awaiting = [];
    // 空閑的子進程
    var readyPool = [];
    // 總子進程的個數
    var poolSize = 0;
    
    return function doWork(job, cb) {
        // 如果現在沒有準備好的子進程,并且總子進程數已經超過cpu的個數了,就讓作業先排隊等待
        if (!readyPool.length && poolSize > cpus) 
            return awaiting.push([ doWork, job, cb ]);
        
        // 如果有空閑的子進程則取出第一個使用,沒有的話就新建一個子進程
        var child = readyPool.length
            ? readyPool.shift()
            : (poolSize++, cp.fork(workModule));
        var cbTriggered = false;
        
        child
            // 先刪除原來的監聽
            .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));
                // 進程關閉的時候將它從隊列中踢出
                poolSize--;
                var childIdx = readyPool.indexOf(child);
                if (childIdx > -1) readyPool.splice(childIdx, 1); 
            })
            .once('message', function(msg) {
                cb(null, msg);
                cbTriggered = true;
                // 子進程再次就緒,將其加回readPool
                readyPool.push(child);
                // 如果現在有等待的任務,運行之
                if (awaiting.length) setImmediate.apply(null, awaiting.shift());
            })
    }
}

在這段代碼的最后我是有一些疑問的,因為當子進程收到message的時候,進行了異步回調。因為當時沒太能分清異步與多線程的區別。所以我當時就不太懂回調都還沒有結束,為什么就能回收這個進程了?

那么這邊也區分一下異步與多線程。

  • 異步:首先說一下DMA,DMA就是直接內存訪問的意思,也就是說,擁有DMA功能的硬件在和內存進行數據交換的時候可以不消耗CPU資源。只要CPU在發起數據傳輸時發送一個指令,硬件就開始自己和內存交換數據,在傳輸完成之后硬件會觸發一個中斷來通知操作完成。這些無須消耗CPU時間的I/O操作正是異步操作的硬件基礎。所以即使在DOS這樣的單進程(而且無線程概念)系統中也同樣可以發起異步的DMA操作。因為異步操作無須額外的線程負擔,并且使用回調的方式進行處理,在設計良好的情況下,處理函數可以不必使用共享變量(即使無法完全不用,最起碼可以減少 共享變量的數量),減少了死鎖的可能。當然異步操作也并非完美無暇。編寫異步操作的復雜程度較高,程序主要使用回調方式進行處理,與普通人的思維方式有些出入,而且難以調試。
  • 多線程:線程不是一個計算機硬件的功能,而是操作系統提供的一種邏輯功能,線程本質上是進程中一段并發運行的代碼,所以線程需要操作系統投入CPU資源來運行和調度。多線程的優點很明顯,線程中的處理程序依然是順序執行,符合普通人的思維習慣,所以編程簡單。但是多線程的缺點也同樣明顯,線程的使用(濫用)會給系統帶來上下文切換的額外負擔。并且線程間的共享變量可能造成死鎖的出現。

詳細可見:淺談多線程和異步

同步運行

其實在異步的API介紹完以后,同步的API就顯得很簡單了。因為它和異步的API基本上是一樣的,只不過在實現的過程當中會阻塞掉主線程,直到子進程模塊完成。

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

    var ex = require('child_process').execFileSync;
    var stdout = ex('echo', ['hello']).toString();
    console.log(stdout);
    
  • 如果想程序式同步執行多個命令,并且命令之間的結果存在相互依賴的關系,可以使用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);
    

    同步子進程得到的結果包含了很多的細節,這也是使用spawnSync的另外一個好處。

  • 當然execSync也是同樣的使用方法,這里不再贅述

同步子進程中的異常處理

如果在execSync或execFileSync執行的結果中返回的是一個非零狀態,這種情況下,將會有異常拋出。這個拋出的異常對象將會包含我們在使用spawnExec返回的結果里的所有東西。我們可以訪問狀態編碼里的重要信息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);
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Node基本 node的最大特性莫過于基于事件驅動的非阻塞I/O模型。 node通過事件驅動的方式處理請求,無須為...
    AkaTBS閱讀 2,215評論 0 11
  • 又來到了一個老生常談的問題,應用層軟件開發的程序員要不要了解和深入學習操作系統呢? 今天就這個問題開始,來談談操...
    tangsl閱讀 4,172評論 0 23
  • https://nodejs.org/api/documentation.html 工具模塊 Assert 測試 ...
    KeKeMars閱讀 6,403評論 0 6
  • ——記2017年初夏14天的晉陜文化之旅 旅游形式;自助旅行。 旅游時間;計劃15天以內,實際14天。 旅游費用...
    田野_13fa閱讀 467評論 0 0
  • 上次賣了個關子,醫學漏洞是什么?不過,一定有朋友對昨天這張圖片感興趣。 為什么會貼上一張烏龜的X光片呢?圖片下的解...
    顛覆醫學閱讀 297評論 0 0