ebookcoin中出現的異步編程淺析

前言

node的核心編程思想是異步編程,ebookcoin是基於node開發的,ebookcoin的異步編程是基於流程控制庫async實現的(當然,不僅僅限於async這個庫,還有其他的方法,因爲是一篇淺析文章,因此,本文不進行分析、討論)。本文將會圍繞着如何利用async完成異步編程開發進行講解,還會對ebookcoin中出現的其他異步編程方式進行膚淺的分析和介紹。

async流程控制庫

async
async是一個流程控制庫,官網網址:http://caolan.github.io/async/。截止到發稿前,async的版本是2.1.4。async提供了三類主要方法,分別是Collections(控制集合的流程控制方法,如何理解呢,簡單的講就是方法的第一個參數可能是一個集合col,或者是一個對象object,或者是一個數組array,通過後邊的代碼來主要操作這第一個參數中的對象)、Control Flow(流程控制方法,標準的流程控制方法)、Utils(流程控制的公共方法)。這三類方法相互協作,共同構建起了async的流程控制庫。

async官網

ebookcoin中出現的async流程控制方法

目前,async的流程控制方法一共有72個,ebookcoin中用到了10個,如下:

Collections方法 說明
eachSeries 別名forEachSeries,格式-eachSeries(coll, iteratee, callbackopt), 該方法和each類似,不過,同一時間只運行一個async的操作,我們將在下方看到例子和講解。
eachLimit 別名eachLimit,格式-eachLimit(coll, limit, iteratee, callbackopt), 該方法和each類似,不過,同一時間只運行最大數量不超過limit個的async操作,我們將在下方看到例子和講解。
forEachOfSeries 別名eachOfSeries,格式-eachOfSeries(coll, iteratee, callbackopt), 該方法和eachOf類似,不過,同一時間只運行一個async的操作,我們將在下方看到例子和講解。
Control Flow方法 說明
auto(自動) 格式-auto(tasks, concurrencyopt, callbackopt)-由庫根據task間的依賴,決定最好的順序來執行tasks,tasks是一個object,裏邊包含了多個子任務和回調函數,我們將在下方看到例子和講解
parallel(並行) 格式-parallel(tasks, callbackopt),我們將在下方看到例子和講解。
series(串行) 格式-series(tasks, callbackopt),我們將在下方看到例子和講解。
whilst(同時) 格式-whilst(test, iteratee, callbackopt),我們將在下方看到例子和講解。
until(直到) 格式-until(test, fn, callbackopt),與whilst相反的一個方法,我們將在下方看到例子和講解。
retry(重試) 格式-retry(optsopt, task, callbackopt),我們將在下方看到例子和講解。
doWhilst 格式-doWhilst(iteratee, test, callbackopt),whilst的 do - while形式,我們將在下方看到例子和講解。

each

eachSeries、eachLimit、eachOfSeries(forEachOfSeries)這三個方法都源於each,因此,先講解一下each。

each的別名是forEach,格式each(coll, iteratee, callbackopt),其中coll是集合、對象或者數組,iteratee是業務函數,重複操作的函數,用來定義一個重複執行的函數,callbackopt是回調函數。

each的功能是並行的將iteratee函數應用於coll的每個元素,並在結束時調用每個元素自己的回調函數(callback()),如果iteratee向自己的callback傳遞了一個error,則外層each方法的callback(可以認爲是外層的那個callback)將馬上接收到這個error。需要注意的是,由於程序是並行運行的,因此,不能保證函數的執行完全遵照順序進行

接下來,我們來看一下例子:

var async = require('async');
var openFiles = ["package.json","markdown.js","abc.js"];

async.each(openFiles, function(file, callback) {

    console.log('Processing file ' + file);

    if( file.length > 32 ) {
        console.log('This file name is too long');
        callback('File name too long');
    } else {

        console.log('File processed');
        callback();
    }
}, function(err) {
    if( err ) {
        console.log('A file failed to process');
    } else {
        console.log('All files have been processed successfully');
    }
});

輸出的結果是:

輸出的結果是

我們可以看到結果是順序執行的,依次是”package.json”,”markdown.js”,”abc.js”,但是,官方文檔給的說明中明確表示,不能100%的保證執行順序,恩,因爲是並行的,此處要注意。

eachSeries

該方法的使用和each一樣,不過,同一時間只運行一個async的操作,但是在實驗的過程中我發現還是有很大區別的,我們來看兩個例子:

//我對原來的函數進行了修改
var async = require('async');
var openFiles = ["package.json","markdown.js","abc.js","abc2.js"];

async.each(openFiles, function(file, callback) {

    console.log('Processing file ' + file);

    if( file.length > 32 ) {
        console.log('This file name is too long');
        callback('File name too long');
    } else if(file==="markdown.js"){

        console.log('File processing');
        setTimeout(function(){console.log('0,File processed')},0);

    }else{
        console.log('File processed');
        callback();
    }
}, function(err) {
    if( err ) {
        console.log('A file failed to process');
    } else {
        console.log('All files have been processed successfully');
    }
});

結果如下:

each

//改爲eachSeries
var async = require('async');
var openFiles = ["package.json","markdown.js","abc.js","abc2.js"];

async.eachSeries(openFiles, function(file, callback) {

    console.log('Processing file ' + file);

    if( file.length > 32 ) {
        console.log('This file name is too long');
        callback('File name too long');
    } else if(file==="markdown.js"){

        console.log('File processing');
        setTimeout(function(){console.log('0,File processed')},0);

    }else{
        console.log('File processed');
        callback();
    }
}, function(err) {
    if( err ) {
        console.log('A file failed to process');
    } else {
        console.log('All files have been processed successfully');
    }
});

eachSeries

我們看到區別了吧,使用eachSeries時,如果出現了中斷的函數,則程序會執行中斷函數中的內容,然後不再向後執行了。但是,這個是現象,原因我就不懂了。找時間,找人討論一下…..

eachLimit

該方法和each的使用類似,不過,同一時間只運行最大數量不超過limit個的async操作。不過當limit=1的時候,eachLimit 和eachSeries是沒有區別的,當limit!=1的時候,eachLimit會更像each。當然,limit=0的時候,這段程序會直接執行callback裏的程序,console.log(‘All files have been processed successfully’);

我們先看limit=0的時候的程序效果:

var async = require('async');
var openFiles = ["package.json","markdown.js","abc.js","abc2.js"];

async.eachLimit(openFiles, 0,function(file, callback) {

    console.log('Processing file ' + file);

    if( file.length > 32 ) {
        console.log('This file name is too long');
        callback('File name too long');
    } else if(file==="markdown.js"){

        console.log('File processing');
        setTimeout(function(){console.log('0,File processed')},0);

    }else{
        console.log('File processed');
        callback();
    }
}, function(err) {
    if( err ) {
        console.log('A file failed to process');
    } else {
        console.log('All files have been processed successfully');
    }
});

效果如下:

limit=0

當limit=1的時候的效果。

var async = require('async');
var openFiles = ["package.json","markdown.js","abc.js","abc2.js"];

async.eachLimit(openFiles, 1,function(file, callback) {

    console.log('Processing file ' + file);

    if( file.length > 32 ) {
        console.log('This file name is too long');
        callback('File name too long');
    } else if(file==="markdown.js"){

        console.log('File processing');
        setTimeout(function(){console.log('0,File processed')},0);

    }else{
        console.log('File processed');
        callback();
    }
}, function(err) {
    if( err ) {
        console.log('A file failed to process');
    } else {
        console.log('All files have been processed successfully');
    }
});

limit=1的時候,與eachSeries相似,我們看結果

eachSeries

limit>1的時候的效果

var async = require('async');
var openFiles = ["package.json","markdown.js","abc.js","abc2.js"];

async.eachLimit(openFiles, 2,function(file, callback) {

    console.log('Processing file ' + file);

    if( file.length > 32 ) {
        console.log('This file name is too long');
        callback('File name too long');
    } else if(file==="markdown.js"){

        console.log('File processing');
        setTimeout(function(){console.log('0,File processed')},0);

    }else{
        console.log('File processed');
        callback();
    }
}, function(err) {
    if( err ) {
        console.log('A file failed to process');
    } else {
        console.log('All files have been processed successfully');
    }
});

limit>1

因此通過上邊的例子我們可以看出,each、eachSeries、eachLimit的區別就是同一時間內有多少個async在操作,each是默認並行的,eachSeries只有一個,eachLimit可以控制async的數量,當然,limit只是理論上的最大值,不一定會有limit個。

eachOf、eachOfSeries

我的程序跑不了這個函數,之後補充,用法就是比each多了一個key作爲iteratee的第二個參數。

var obj = {dev: "/dev.json", test: "/test.json", prod: "/prod.json"};
var configs = {};

async.forEachOf(obj, function (value, key, callback) {
    fs.readFile(__dirname + value, "utf8", function (err, data) {
        if (err) return callback(err);
        try {
            configs[key] = JSON.parse(data);
        } catch (e) {
            return callback(e);
        }
        callback();
    });
}, function (err) {
    if (err) console.error(err.message);
    // configs is now a map of JSON data
    doSomethingWith(configs);
});

這個key是遍歷的遊標,通過key可以記錄循環到了list的哪個對象了。

auto的例子

該方法的格式爲auto(tasks, concurrencyopt, callbackopt)
auto這個方法有幾個特性
1.auto會決定tasks中的函數和依賴的一個最好的執行順序,每個函數都可以選擇是否需要完成前置任務。
2.如果一個函數產生錯誤,則整個流程都會停止,其他的任務將不會被執行,auto的callback將會馬上收到這個錯誤,並結束整個auto流程。
3.當前置任務,完成之後,會將結果傳遞到依賴他的函數中,如果沒有依賴,則只會調用該任務的callback

在ebookcoin中,app.js加載服務器的方式就是利用了auto這個方法。好了,我們來看一個例子

async.auto({
    // this function will just be passed a callback
    readData: async.apply(fs.readFile, 'data.txt', 'utf-8'),
    showData: ['readData', function(results, cb) {
        // results.readData is the file's contents
        // ...
    }]
}, callback);

async.auto({
    get_data: function(callback) {
        console.log('in get_data');
        // async code to get some data
        callback(null, 'data', 'converted to array');
    },
    make_folder: function(callback) {
        console.log('in make_folder');
        // async code to create a directory to store a file in
        // this is run at the same time as getting the data
        callback(null, 'folder');
    },
    write_file: ['get_data', 'make_folder', function(results, callback) {
        console.log('in write_file', JSON.stringify(results));
        // once there is some data and the directory exists,
        // write the data to a file in the directory
        callback(null, 'filename');
    }],
    email_link: ['write_file', function(results, callback) {
        console.log('in email_link', JSON.stringify(results));
        // once the file is written let's email a link to it...
        // results.write_file contains the filename returned by write_file.
        callback(null, {'file':results.write_file, 'email':'[email protected]'});
    }]
}, function(err, results) {
    console.log('err = ', err);
    console.log('results = ', results);
});

auto的執行順序

parallel的例子

該方法的格式爲:parallel(tasks, callbackopt)
該方法的特性如下:
1.並行運行tasks中的函數,並將執行結構以數組的形式保存到callback中
2.任意一個函數報錯,都會向parallel的callback傳遞一個error。(官網沒說會不會停止整個方法)
3.parallel是一個kicking-off I/O,而不是並行執行的代碼,也就是說,如果沒有人爲添加的並行操作,則該功能就是一個順序執行的函數,如果存在異步執行的函數,纔會並行的去執行。(很拗口吧,一會看例子)

async.parallel([
        function(callback) {
            setTimeout(function() {
                callback(null, 'one');
            }, 200);
        },
        function(callback) {
            setTimeout(function() {
                callback(null, 'two');
            }, 100);
        }
    ],
// optional callback
    function(err, results) {
        // the results array will equal ['one','two'] even though
        // the second function had a shorter timeout.
        console.log('results = ', results);
    });

// an example using an object instead of an array
async.parallel({
    one: function(callback) {
        setTimeout(function() {
            callback(null, 1);
        }, 200);
    },
    two: function(callback) {
        setTimeout(function() {
            callback(null, 2);
        }, 100);
    }
}, function(err, results) {
    console.log('results = ', results);
});

parallel

series

格式:series(tasks, callbackopt)
特性:
1.順序執行tasks中的函數,tasks可以是對象(返回對象到callback)或者數組(返回數組到callback)
2.一個函數報錯則方法停止,傳遞一個error到callback
3.因爲ES5的特殊性,如果想在各個平臺上都是按照自己定義的順序去執行函數的話,最好還是用array in tasks

我們來看一個例子:

async.series([
        function(callback) {
            // do some stuff ...
            callback(null, 'one');
        },
        function(callback) {
            // do some more stuff ...
            callback(null, 'two');
        }
    ],
// optional callback
    function(err, results) {
        // results is now equal to ['one', 'two']
        console.log('results = ', results);
    });

async.series({
    one: function(callback) {
        setTimeout(function() {
            callback(null, 1);
        }, 200);
    },
    two: function(callback){
        setTimeout(function() {
            callback(null, 2);
        }, 100);
    }
}, function(err, results) {
    // results is now equal to: {one: 1, two: 2}
    console.log('results = ', results);
});

series

從例子中,我們看到使用series的好處了吧,可以最大限度的將異步的程序寫成同步的樣式。

whilst、doWhilst

格式:whilst(test, iteratee, callbackopt)
特性:只要test返回true,就重複調用iteratee,有錯也停止,看例子:

var count = 0;
async.whilst(
    function() { return count < 5; },
    function(callback) {
        count++;
        setTimeout(function() {
            callback(null, count);
        }, 30);
    },
    function (err, n) {
        // 5 seconds have passed, n = 5
        console.log(n);
    }
);

這個例子我這個程序一直返回undefined,不知道什麼原因,因此,不截圖了

doWhilst的格式:doWhilst(iteratee, test, callbackopt)

doWhilst和whilst的關係,跟do while 和 while的關係一樣,都是先執行一次,另外一個不同的地方是,參數的位置不一樣,呵呵呵

until

格式:until(test, fn, callbackopt)
特性:
1.不斷的執行fn,直到test 返回true。
2.方法存在錯誤將會停止,並向fn的callback傳遞error。

用法與whilst相反。

retry

格式爲:retry(optsopt, task, callbackopt)
特性:
1.嘗試從task中得到正確的響應
2.有錯則會嘗試最後一次函數調用

我們來看格式:

// The `retry` function can be used as a stand-alone control flow by passing
// a callback, as shown below:

// try calling apiMethod 3 times
async.retry(3, apiMethod, function(err, result) {
    // do something with the result
});

// try calling apiMethod 3 times, waiting 200 ms between each retry
async.retry({times: 3, interval: 200}, apiMethod, function(err, result) {
    // do something with the result
});

// try calling apiMethod 10 times with exponential backoff
// (i.e. intervals of 100, 200, 400, 800, 1600, ... milliseconds)
async.retry({
  times: 10,
  interval: function(retryCount) {
    return 50 * Math.pow(2, retryCount);
  }
}, apiMethod, function(err, result) {
    // do something with the result
});

// try calling apiMethod the default 5 times no delay between each retry
async.retry(apiMethod, function(err, result) {
    // do something with the result
});

// try calling apiMethod only when error condition satisfies, all other
// errors will abort the retry control flow and return to final callback
async.retry({
  errorFilter: function(err) {
    return err.message === 'Temporary error'; // only retry on a specific error
  }
}, apiMethod, function(err, result) {
    // do something with the result
});

// It can also be embedded within other control flow functions to retry
// individual methods that are not as reliable, like this:
async.auto({
    users: api.getUsers.bind(api),
    payments: async.retry(3, api.getPayments.bind(api))
}, function(err, results) {
    // do something with the results
});

我們再來看ebookcoin中的例子:

    async.retry(20, function (cb) {
        modules.peer.list(config, function (err, peers) {
            if (!err && peers.length) {
                var peer = peers[0];
                self.getFromPeer(peer, options, cb);
            } else {
                return cb(err || "No peers in db");
            }
        });
    }, function (err, results) {
        cb(err, results);
    });

ebookcoin程序的意思是,在不報錯的前提下,最多將這個匿名函數執行20次。self.getFromPeer(peer, options, cb);這個功能是尋找網絡中的peer節點。

總結

本文簡單介紹了async庫以及ebookcoin用了async的哪些方法,並對這些方法從流程控制的角度進行了淺析。node的異步編程博大精深,ebookcoin的源碼也精彩絕倫(crypti的源碼就寫的很好了,通過對ebookcoin和crypti的源碼,給了我在程序設計和實現上很多的啓發,真好,能站在巨人們的肩膀上,真好),本文僅僅算是拋磚引玉的文章,希望通過該文激起大家學習node,使用node的興起,同時,也需要大家多提意見,我們共同成長,歡迎越來越多的小夥伴們進入到node領域,進入到區塊鏈研究的領域。

後記

下一篇文章計劃解讀一下ebookcoin的編解碼處的代碼,當然都是淺析,都是先從最基本的技術實現入手進行解讀,我希望自己能夠堅持下來,爭取一週能寫一篇文章。

歡迎大家添加好友,作者本人的微信:(作者:Ryan)

Ryan

歡迎大家添加關注,作者本人的微信公衆號:(作者:Ryan)

Ryan

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章