03月14, 2018

JavaScript 异步编程

JavaScript 异步编程

异步的概念

  作为一个java程序猿,此处需要明确的是:异步与多线程与并行不是同一个概念,所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。 比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。

js的异步非阻塞

nodejs强调的一点就是基于事件的异步非阻塞,实现的方式都是通过回调函数callback,比如读取一个文件。但是异步非阻塞也并不代表着node是多线程执行的,对于一些API,比如读取文件,操作OS系统,这些操作最后其实都不是nodejs在执行,所以他们是不是多线程由运行环境绝定,大多数情况下是多线经行的;但是如果是自己纯js实现的异步非阻塞,比如promise的then方法就是异步非阻塞的,但是此时也只有一个node线程运行它。

var fs = require('fs');

fs.readFile('json.txt',function (err, str) {
    console.log(str.toString())
});

fs.readFile在文件读取结束后会把结果放到回调函数中,而不会阻塞fs.readFile后边的代码,这样就实现了类似多线程的效果。

js异步阻塞

异步阻塞就是指的单纯的一个操作分隔到不同的时候执行,但是所有的执行操作都是阻塞的。

function* gen() {
    let a = yield console.log("第一步操作");
    let b = yield console.log(`第三步操作 ${a}`);
    console.log(`第五步操作 ${a+b}`)
}
let g = gen();
g.next()
console.log("第二步操作")
g.next('hello')
console.log("第四步操作")
g.next('world')
/**
    第一步操作
    第二步操作
    第三步操作 hello
    第四步操作
    第五步操作 helloworld
 */

js的Promise实现异步非阻塞

node的异步非阻塞是基于回调函数实现的,但是并不代表所有的回调函数都是异步的,除了其他模块提供的异步函数接口,一般情况下所有的回调函数也是同步阻塞执行的。如果我们需要异步非阻塞的处理某些操作,可以通过Promise的then函数来实现。

var fs = require('fs');
//使用fs.readFileSync这个同步方法实现异步fs.readFile的功能
let str = fs.readFileSync("json.txt");
console.log(str.toString())

readFileAsync("json.txt",function (str) {
    let obj = JSON.parse(str.toString());
    obj.name = "李四"
    obj.age = 81
    console.log(obj)
});

function readFileAsync(path,callback) {
    Promise.resolve().then(function () {
        let str = fs.readFileSync("json.txt");
        callback(str)
    })
}
console.log("================")

{
    "name": "张三",
    "age": 18
}
================
{ name: '李四', age: 81 }

从上边的例子中可以看出,fs.readFileSync本来是同步阻塞的函数,但是我们把它包装在了Promise.resolve().then()方法的回调函数中之后,它变成了异步函数。

js异步函数同步表达

使用 Generator 函数实现

由于nodejs的理念是异步非阻塞,这样致使很多的API只提供的异步的版本,但是实际应用这很多时候并不需要异步,而且多个异步函数的时候很容易导致出现回调函数地狱(callback hell)导致代码几乎不可维护,所以此时就需要把异步函数来进行同步化表示,方便经行管理,提高代码的可阅读性。

var fs = require('fs');

let readFileThunk = thunkify(fs.readFile);
function* gen() {
    var f1 = yield readFileThunk('json1.txt');
    // callback 读取完 ‘json1.txt’ 需要执行的操作
    var f2 = yield readFileThunk('json.txt');
    console.log(f1.toString());
    console.log(f2.toString());
}
co(gen);

//thunkify 模块
function thunkify(fn){
    if('function' != typeof fn){
        throw new TypeError('function required');
    }

    return function(){
        var args = new Array(arguments.length);
        var ctx = this;

        for(var i = 0; i < args.length; ++i) {
            args[i] = arguments[i];
        }

        return function(done){
            var called;

            args.push(function(){
                if (called) return;
                called = true;
                done.apply(null, arguments);
            });

            try {
                fn.apply(ctx, args);
            } catch (err) {
                done(err);
            }
        }
    }
};

//简单的co模块
function co(fn) {
    let gen = fn();
    //自动执行next,同时兼顾了callback的作用
    function next(err, data) {

        let result = gen.next(data);


        if (result.done) return;
        result.value(next);
    }
    next();
}

/**
     {
        "name": "李四",
        "age": 81
    }
    {
        "name": "张三",
        "age": 18
    }
 */

如果js基础不是很牢靠,上叙代码乍一看有点迷糊,因为设计到的知识点比较多,我们先慢慢一步一步解析它。首先是thunkify模块,它里边有两个return语句。我们拿正常的异步读取文件举例子,正常情况下如下代码

var fs = require('fs');
fs.readFile('json.txt',callback);

而使用thunkify模块后,thunkify()只接受一个函数作为参数,然后返回一个接受原函数参数的函数readFileThunk(),最后返回一个只接受callback回调的函数callbackExecutor(),最后把回调放到callbackExecutor()中,就完成了整个执行过程。

let readFileThunk = thunkify(fs.readFile);
let callbackExecutor = readFileThunk('json.txt');
callbackExecutor(callback);

这样做表面上看是把简单是事情复杂化了,但是,它把两个参数分开了,可以在不同的位置执行。我们再回到原来的例子上。

  • (1)在co模块中generator函数生成了对象gen.
  • (2)然后调用了next()方法,next()返回了readFileThunk('json1.txt');的值callbackExecutor作为result.value.
  • (3)然后又把next()作为callbackExecutor的参数,读取第一个文件完毕自动调用next(err,data),此时会把读取结果传进了.
  • (4)然后又通过gen.next(data)var f1 = yieldjson1.txt读取到的值赋给了f1,到这里第一个文件读取完毕,然后读取第二个文件也是一样的步骤.

本来是异步的操作,经过这么一转换编程了同步执行的了。 alt

使用async 函数来实现

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

//Generator 函数 + Promise = async
function spawn(genF) {
    return new Promise(function (resolve, reject) {
        const gen = genF();

        function step(nextF) {
            let next;
            try {
                next = nextF();
            } catch (e) {
                return reject(e);
            }
            if (next.done) {
                return resolve(next.value);
            }
            Promise.resolve(next.value).then(function (v) {
                step(function () {
                    return gen.next(v);
                });
            }, function (e) {
                step(function () {
                    return gen.throw(e);
                });
            });
        }

        step(function () {
            return gen.next(undefined);
        });
    });
}

async 简单使用

const fs = require('fs');

const readFile = function (fileName) {
    return new Promise(function (resolve, reject) {
        fs.readFile(fileName, function(error, data) {
            if (error) return reject(error);
            resolve(data);
        });
    });
};

const asyncReadFile = async function () {
    const f1 = await readFile('json.txt');
    const f2 = await readFile('json1.txt');
    console.log(f1.toString());
    console.log(f2.toString());
};
asyncReadFile()
/**
     {
        "name": "张三",
        "age": 18
    }
     {
         "name": "李四",
         "age": 81
     }
 */


 //=============================
 const fs = require('fs');

const readFile = function (fileName) {
    return new Promise(function (resolve, reject) {
        fs.readFile(fileName, function(error, data) {
            if (error) return reject(error);
            resolve(data);
        });
    });
};

const asyncReadFile = async function () {
    const f1 = await readFile('json.txt');
    const f2 = await readFile('json1.txt');
    return {
        f1 : f1.toString(),
        f2 : f2.toString()
    }
};
let promise = asyncReadFile();

promise.then(function (value) {
    console.log(value.f1)
    console.log(value.f2)
})

js实现多线程的效果

Future-task 模式

  • Promise 实现
var fs = require('fs');

//读取文件
console.log("读取文件")
let readFilePromise = Promise.resolve().then(function () {
    let str = fs.readFileSync("json.txt");
    for (let i = 0; i < 100000*1000; i++) {
        //模拟耗时
        sum = sum + i;
    }
    return str.toString();
});

console.log("做其他事")
let sum = 0;
for (let i = 0; i < 100000*900; i++) {
    //模拟另外一件事耗时
    sum = sum + i;
}

//处理结果
readFilePromise.then(function (str) {
    console.log(`处理结果:sum ${sum}, str:${str}`)
});
/**
     读取文件
     做其他事
     处理结果:sum 4049999955000000, str:{
        "name": "张三",
        "age": 18
     }
 */
  • Generator 函数实现
function* main() {
    let result = yield doSomethingAsync();
    let resp = result.value;
    resp.then(function (value) {
        console.log(`异步操作返回结果 ${value}`)
        console.log("结束")
    })
}
function doSomethingAsync() {
    return Promise.resolve().then(function () {
        for (let i = 0; i < 1000000; i++) {
        }
        console.log("异步操作处理完毕")
        return "hello";
    })
}
var it = main();
console.log("开始")
//开始异步操作,并获取异步结果
let value = it.next();
//...进行其他操作处理
console.log("进行其他操作处理")
//处理异步的结果
it.next(value);
/**
 开始
 进行其他操作处理
 异步操作处理完毕
 异步操作返回结果 hello
 结束
 */

Master-worker 模式

Master-worker是一种分布式计算的设计,但是对于nodejs这种单线程的应用来说,貌似并没有什么卵用。下列例子中使用Master-worker模式异步计算所消耗的时间和直接计算执行的时间相差无几。这也证明了,nodejs确实使用的是单线在跑的。

//计算从0到 10000000*10的和
let startOn = Date.now();
function Woker(maxValue,name) {
    return Promise.resolve().then(function () {
        let start = ((maxValue/10000000 - 1)<0?0:(maxValue/10000000 - 1))*10000000;
        let sum = 0;
        for (let i = start; i < maxValue; i++) {
            sum = sum + i;
        }
        console.log(`${name} 计算${start}-${maxValue}完毕,值为${sum}`)
        return sum;
    });
}

let wokers = [];
for (let i = 1; i <= 10; i++) {
    let woker = new Woker(i*10000000,'worker'+i);
    wokers.push(woker);
}
console.log("=========我出现在第一行证明是异步计算的==========")

Promise.all(wokers).then(function (sums) {
    let sum = 0;
    for (let obj of sums) {
        sum = sum + obj;
    }
    console.log(``)
    console.log(`最终结果:${sum},耗时:${Date.now() - startOn}`) //最终结果:4999999950000000,耗时:194
})


//===================直接计算=======================
/*
let sum = 0;
for (let i = 0; i < 10000000*10; i++) {
    sum = sum + i;
}
console.log(`最终结果:${sum},耗时:${Date.now() - startOn}`)//最终结果:4999999950000000,耗时:193*/

生产者消费者模式

js是个单线程应用,一般情况下不会有生产者-消费者这种并发模型,事实上它也做不了负责的多生产者和多消费者。nodejs提供了一种事件驱动的模式,我们现在可以用利用这个实现一个简单的生产者-消费者模型。

const EventEmitter = require('events')

const MyEmitter = class extends EventEmitter{

}

let queue = [];
let count = 0;
const QUEUE_MAX_LENGTH = 500;

const myEmitter = new MyEmitter();
// 消费者
let consumer = {
    consume(queue){
        setTimeout(function () {
            if(queue.length > 0){
                console.log(`消费------${queue.pop()}`);
                myEmitter.emit('produce');
            }else {
                myEmitter.emit('produce');
            }
        },500)
    }
};
// 生产者
let producer = {
    produce(queue){
        setTimeout(function () {
            if(queue.length <= QUEUE_MAX_LENGTH){
                let items = `产品${count++}`;
                queue.push(items);
                console.log(`生产------${items}`)
                myEmitter.emit('consume');
            }else{
                myEmitter.emit('consume');
            }
        },500)
    }
};

myEmitter.on('consume', () => {
    consumer.consume(queue);
});


myEmitter.on('produce', () => {
    producer.produce(queue);
});

setInterval(function () {
    producer.produce(queue);
},1000)

本文链接:https://www.qiangshuidiyu.xin/post/js-async.html

-- EOF --

Comments