js异步之惑

js异步之惑

1.异步是啥

与异步对应的就是同步,对于同步我们很好理解,就是代码顺序执行。但是一到异步代码,很多人多少有些理不清。异步,从功能上讲,就是在背后偷偷的执行,不堵塞当前运行的代码;从实现上讲,能够这么做的,就只能靠在当前运行代码中另一起线程或者进程了。举一个使用线程来实现异步的例子:

public class MyAsync extends Thread {
        private volatile boolean done = false;

        public void run() {
            while (!done) {//子线程中的循环
                System.out.println("thread out x");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
    //              Thread.currentThread().interrupt();
                }
            }
        }

        public synchronized void setDone() {
            done = true;
        }

        public static void main(String argv[]) {
            MyAsync t = new MyAsync();
            t.start();//起子线程
            long last = System.currentTimeMillis();
            int count = 0;
            while(true) {//主线程中循环
                long now = System.currentTimeMillis();
                if (now - last > 1000 * 1) {
                    last = now;
                    count ++;
                    System.out.println("the " + count + "th count.");
                }
                if (count > 2) {
                    break;
                }

            }

            t.setDone();
        }
    }

代码1.1 线程示例

对于代码1.1的运行结果,有可能是这个样子的:

thread out x
thread out x
thread out x
the 1th count.
thread out x
thread out x
the 2th count.
thread out x
thread out x
the 3th count.

代码27-38行起是主线程循环,7-15行是子线程循环,你多运行几次,就可以看出两个循环的输出是很随机的,但是不管运行多少次两个循环的输出都是交叉在一起的。这样我们就可以推断出,运行主线程的代码的时候,子线程的代码也在背后偷偷的运行,说白了两者是并行运行的。

2.js中的异步

就是因为异步这个特性,js如今被大家推崇,下面用一个小例子来演示一下js中异步的使用:

    function synchronizedCode() {
        var last = new Date().getTime();
        var count = 0;
        while (true) {
            var now = new Date().getTime();
            if (now - last > 1000 * 2) {
                last = now;
                count++;
                console.log(‘the %dth count.‘,count);
            }
            if (count > 9) {
                console.log(‘exist while.‘);
                break;
            }
        }
    }
    (function() {
        setTimeout(function() {console.log(‘setTimeout 0 occured first.‘);},0);
        setTimeout(function() {console.log(‘setTimeout 0 occured second.‘);},0);

        synchronizedCode();
    })();

代码2.1 setTimeout的例子

我们运行代码2.1,然后不管运行多少次,输出都是这个样子的:

the 1th count.
the 2th count.
the 3th count.
the 4th count.
the 5th count.
exist while.
setTimeout 0 occured first.
setTimeout 0 occured second.

输出2.1

跟java中的异步和同步代码会交叉输出相比,js中的异步其实是排好队输出的。由于js是单线程执行代码的,所以没有那种交叉输出的效果。那么还有一个问题,while循环明明运行了5秒钟,为何在这期间那两个setTimeout一直没有运行呢?这就和js代码的异步机制有关了。js代码中有帧的概念,对于同步代码是在当前帧运行的,异步代码是在下一帧运行的。对于代码2.1我们给代码运行画一幅图的话,应该是这样的:

图2.1 js帧结构

那么为什么是第一个setTimeout先触发,第二个后触发呢,难道仅仅由于先后顺序?我们把第一个setTimeout改为setTimeout(function() {console.log(‘setTimeout 0 occured first.‘);},100);,那么输出的时候就会是先输出setTimeout 0 occured second.,在输出setTimeout 0 occured first.。也就是说在第二帧setTimeout的回调的执行顺序不仅与代码顺序有关还和延迟时间有关。

在node.js中还有一个特殊的API,就是process.nextTick,虽然已经不推荐使用了,但是已经可以在很多代码中看到它的身影。例如如下代码:

    (function() {
        setTimeout(function() {console.log(‘setTimeout 0 occured first.‘);},0);
        setTimeout(function() {console.log(‘setTimeout 0 occured second.‘);},0);
        process.nextTick(function() {console.log(‘nextTick occured.‘);});

        synchronizedCode();
    })();

代码2.2

运行后输出:

the 1th count.
the 2th count.
the 3th count.
the 4th count.
the 5th count.
exist while.
nextTick occured.
setTimeout 0 occured first.
setTimeout 0 occured second.

输出2.2

之所以nextTick排在所有异步的最前面,是由于nextTick是在第一帧运行的,而其他的都是在第二帧运行的。也就是说代码运行情况是这个样子的:

图2.2 js帧结构

接下来再举几个异步API的例子,这次我们添加setImmediatemkdir两个函数:

    var synchronizedCode = require(‘./sync‘);
    (function() {
        setTimeout(function() {console.log(‘setTimeout 0 occured first.‘);},0);
        setTimeout(function() {console.log(‘setTimeout 0 occured second.‘);},0);
        process.nextTick(function() {console.log(‘nextTick occured.‘);});
        setImmediate(function() {console.log(‘setImmediate occured.‘)});

        var fs = require(‘fs‘);
        var crypto = require(‘crypto‘);
        var rand = crypto.pseudoRandomBytes(8).toString(‘hex‘);
        fs.mkdir(‘d:\\temp\\xx‘+‘\\‘+rand,function(err) {
            if (err) {
                console.log(err,‘错误‘,err.code);
            } else {
                console.log(‘create directory success.‘);
            }
        });

        synchronizedCode();
    })();  

代码2.3

那么他的输出就应该是这样的:

the 1th count.
the 2th count.
the 3th count.
the 4th count.
the 5th count.
exist while.
nextTick occured.
setTimeout 0 occured first.
setTimeout 0 occured second.
setImmediate occured.
create directory success.

输出2.3

等等,问题来了,这里最后一句才打印create directory success,那么是不是程序是在最后一步才创建的文件夹呢,如果真是这样,就有点低效了,起码这个创建文件的工作被那个while循环给延迟了得有5秒钟。不过幸好,这个想法是错误的!node.js中使用libuv来IO或者CPU计算量大的操作,而在libuv中处理这些耗时的操作都是用线程来解决,以避免堵塞住js线程(这一点和android的程序设计思路类似,android开发中使用子线程来处理耗时逻辑,避免对主线程造成卡顿)。这里我们来演示一个libuv的异步处理,在异步处理中模拟一个耗时操作:

    #include <node.h>
    #include <string>

    #ifdef WINDOWS_SPECIFIC_DEFINE
    #include <windows.h>
    typedef DWORD ThreadId;
    #else
    #include <unistd.h>
    #include <pthread.h>
    typedef unsigned int ThreadId;
    #endif
    using namespace v8;

    Handle<Value> async_hello(const Arguments& args);

    //不在js主线程,,在uv线程池内被调用
    void call_work(uv_work_t* req);

    //回调函数
    void call_work_after(uv_work_t* req);

    static ThreadId __getThreadId() {
        ThreadId nThreadID;
    #ifdef WINDOWS_SPECIFIC_DEFINE

        nThreadID = GetCurrentProcessId();
        nThreadID = (nThreadID << 16) + GetCurrentThreadId();
    #else
        nThreadID = getpid();
        nThreadID = (nThreadID << 16) + pthread_self();
    #endif
        return nThreadID;
    }

    static void __tsleep(unsigned int millisecond) {
    #ifdef WINDOWS_SPECIFIC_DEFINE
        ::Sleep(millisecond);
    #else
        usleep(millisecond*1000);
    #endif
    }

    //定义一个结构体,存储异步请求信息
    struct Baton {

        //存储回调函数,使用Persistent来声明,让系统不会在函数结束后自动回收
        //当回调成功后,需要执行dispose释放空间
        Persistent<Function> callback;

        // 错误控制,保护错误信息和错误状态
        bool error;
        std::string error_message;

        //存储js传入的参数,字符串
        std::string input_string;

        //存放返回参数,字符串
        std::string result;

    };

    Handle<Value> async_hello(const Arguments& args) {
        printf("\n%s Thread id : gettid() == %d\n",__FUNCTION__,__getThreadId());
        HandleScope scope;
        if(args.Length() < 2) {
            ThrowException(Exception::TypeError(String::New("Wrong number of arguments")));
            return scope.Close(Undefined());
          } 

        if (!args[0]->IsString() || !args[1]->IsFunction()) {
            return ThrowException(Exception::TypeError(
                String::New("Wrong number of arguments")));
        }

        // 强制转换成函数变量
        Local<Function> callback = Local<Function>::Cast(args[1]);

        Baton* baton = new Baton();
        baton->error = false;
        baton->callback = Persistent<Function>::New(callback);
        v8::String::Utf8Value param1(args[0]->ToString());
        baton->input_string = std::string(*param1); 

        uv_work_t *req = new uv_work_t();
        req->data = baton;

        int status = uv_queue_work(uv_default_loop(), req, call_work,
                                   (uv_after_work_cb)call_work_after);
        assert(status == 0);
        return Undefined();
    }

    //在该函数内模拟处理过程 ,如i/o阻塞或者cpu高消耗情形的处理。
    // 注意不能使用v8 api,这个线程不是在js主线程内
    void call_work(uv_work_t* req) {
        printf("\n%s Thread id : gettid() == %d\n",__FUNCTION__,__getThreadId());
        Baton* baton = static_cast<Baton*>(req->data);
        for (int i=0;i<15;i++) {
            __tsleep(1000);
            printf("sleep 1 seconds in uv_work\n");
        }

        baton->result = baton->input_string+ "--->hello world from c++";

    }

    //执行完任务,进行回调的时候,返回到 js主线程
    void call_work_after(uv_work_t* req) {
        printf("\n%s Thread id : gettid() == %d\n",__FUNCTION__,__getThreadId());
        HandleScope scope;
        Baton* baton = static_cast<Baton*>(req->data);
        if (baton->error) {
            Local<Value> err = Exception::Error(String::New(baton->error_message.c_str()));

            // 准备回调函数的参数
            const unsigned argc = 1;
            Local<Value> argv[argc] = { err };

            //捕捉回调中的错误,在Node环境中可利用process.on(‘uncaughtException‘)捕捉错误
            TryCatch try_catch;

            baton->callback->Call(Context::GetCurrent()->Global(), argc, argv);
            if (try_catch.HasCaught()) {
                node::FatalException(try_catch);
            }
        } else {

            const unsigned argc = 2;
            Local<Value> argv[argc] = {
                Local<Value>::New(Null()),
                Local<Value>::New(String::New(baton->result.c_str()))
            };
            TryCatch try_catch;
            baton->callback->Call(Context::GetCurrent()->Global(), argc, argv);
            if (try_catch.HasCaught()) {
                node::FatalException(try_catch);
            }
        }

        //注意一定释放
        baton->callback.Dispose();
        // 处理完毕,清除对象和空间
        delete baton;
        delete req;
    }

    void RegisterModule(Handle<Object> target) {
        target->Set(String::NewSymbol("async_hello"),FunctionTemplate::New(async_hello)->GetFunction());
    }

    NODE_MODULE(binding, RegisterModule);  

代码2.4

编写的测试代码,将其运行,就可以看出函数call_work根本就不在js线程中执行,也就是说它是可以和js线程并行的;函数call_work_after中能够触发js中的回调函数,将处理完的结果交给js。下面就编译上面代码(需要node-gyp支持,执行node-gpy rebuild进行编译),来验证上述结论:

    (function() {
        setTimeout(function() {synchronizedCode(‘timer1‘,3);},0);
        setTimeout(function() {console.log(‘setTimeout 0 occured second.‘);},0);
        process.nextTick(function() {console.log(‘nextTick occured.‘);});

        var addon = require(‘./addon/build/Release/binding‘);
        addon.async_hello("good",function(err, result) {
            console.log(‘node addon result‘,result);
        });

        setImmediate(function() {console.log(‘setImmediate occured.‘)});

        var fs = require(‘fs‘);
        var crypto = require(‘crypto‘);
        var rand = crypto.pseudoRandomBytes(8).toString(‘hex‘);
        fs.mkdir(‘d:\\temp\\xx‘+‘\\‘+rand,function(err) {
            if (err) {
                console.log(err,‘错误‘,err.code);
            } else {
                console.log(‘create directory success.‘);
            }
        });

        synchronizedCode();
    })();  

代码2.5

我们在代码2.5中引入了代码2.4中的c++扩展,其输出内容如下

async_hello Thread id : gettid() == 284953360

call_work Thread id : gettid() == 284958096
sleep 1 seconds in uv_work
sleep 1 seconds in uv_work
the 1th count.
sleep 1 seconds in uv_work
sleep 1 seconds in uv_work
the 2th count.
sleep 1 seconds in uv_work
sleep 1 seconds in uv_work
the 3th count.
sleep 1 seconds in uv_work
sleep 1 seconds in uv_work
the 4th count.
sleep 1 seconds in uv_work
sleep 1 seconds in uv_work
the 5th count.
exist while.
nextTick occured.
sleep 1 seconds in uv_work
sleep 1 seconds in uv_work
the 1th count in [timer1].
sleep 1 seconds in uv_work
sleep 1 seconds in uv_work
the 2th count in [timer1].
sleep 1 seconds in uv_work
the 3th count in [timer1].
the 4th count in [timer1].
exist while in [timer1].
setTimeout 0 occured second.
setImmediate occured.
create directory success.

call_work_after Thread id : gettid() == 284953360
node addon result good--->hello world from c++

输出2.5

我们终于看到开篇提到的类似java代码的交叉输出的效果了。libuv在处理任务时根本就和js不在一个线程中,所以才出现了libuv线程和js线程交叉输出的效果。我们在梳理一下代码2.5的异步流程,那么可以用下面这个图来展示出来:

图2.3

在node中维护了一个回调的队列,那为什么调用插件的回调排在队列的最后面呢,是有于我们在代码2.4中故意将其代码设置成15秒之后才执行完成,这要远远长于其他的那些回调,所以它只能被追加到回调队列的最后一位。在第二帧中,node中的事件轮询依次将这些回调队列中的任务取出来,进行触发调用。可以看出回调队列是个先进先出的结构。注意回调是按照队列中排队顺序执行的,同时它们执行的环境是js线程,要知道js线程可是个单线程,也就是说一旦某个回调中的同步代码耗时比较长,那么它后面的回调任务就得一直等着它运行完成,所以说一定不要在回调中写特别耗时的同步代码。

代码

本文用到代码发布在http://git.oschina.net/yunnysunny/async-tutorial-code

时间: 2024-10-11 09:37:08

js异步之惑的相关文章

Javascript教程:js异步编程的4种方法详述(转载)

文章收集转载于(阮一峰的网络日志) 你可能知道,Javascript语言的执行环境是“单线程”(single thread). 所谓“单线程”,就是指一次只能完成一件任务.如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推. 这种模式的好处是实现起来比较简单,执行环境相对单纯:坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行.常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方

页面优化——js异步加载

同步加载 在介绍js异步加载之前,我们先来看看什么是js同步加载.我们平时最常使用的就是这种同步加载形式: <script src="http://XXX.com/script.js"></script> 同步模式,又称阻塞模式,会阻止浏览器的后续处理,停止了后续的解析,因此停止了后续的文件加载(如图像).渲染.代码执行.一般的script标签(不带async等属性)加载时会阻塞浏览器,也就是说,浏览器在下载或执行该js代码块时,后面的标签不会被解析,例如在he

js异步加载 defer和async 比较

网上说法很多,很少一句话能总结清楚的,终于找到两句一针见血的描述,很到位: 相同点:都不阻塞DOM解析 defer  :顺序:保证先后顺序.解析:HTML 解析器遇到它们时,不阻塞(脚本将被异步下载),待文档解析完成之后,执行脚本. async  :顺序:不保证先后顺序.解析:HTML 解析器遇到它们时,不阻塞(脚本将被异步下载,一旦下载完成,立即执行它),并继续解析之后的文档. 总结一下:defer 效果是 :js异步下载完毕后且DOM解析完成后且DOMContentLoaded 事件触发之前

js 异步流程控制之 avQ(avril.queue)

废话前言 写了多年的js,遇到过最蛋疼的事情莫过于callback hell, 相信大家也感同身受. 业界许多大大也为此提出了很多不错的解决方案,我所了解的主要有: 朴灵 event proxy, 简单明了容易上手 老赵的 wind.js, 写起来最舒坦,最能表达程序顺序执行逻辑 Promise,个人感觉为解决一个坑引入另外一个坑,写出来的代码一大坨,代码可读性最差 我这人闲着没事也爱折腾,我也自己造轮子,不为别的只为自己代码写的舒服流畅. 传送门:目前只支持 node.js 环境,以后有时间再

node js 异步执行流程控制模块Async介绍

1.Async介绍 sync是一个流程控制工具包,提供了直接而强大的异步功能.基于Javascript为Node.js设计,同时也可以直接在浏览器中使用. Async提供了大约20个函数,包括常用的 map, reduce, filter, forEach 等,异步流程控制模式包括,串行(series),并行(parallel),瀑布(waterfall)等. 项目地址:https://github.com/caolan/async 2. Async安装 npm install async 3.

node js异步IO机制

同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行:而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数. 阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间:而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成. node js的异步I/O是它的一个重要功能,为了讲清楚这个机制,先说

js异步编程

前言 以一个煮饭的例子开始,例如有三件事,A是买菜.B是买肉.C是洗米,最终的结果是为了煮一餐饭.为了最后一餐饭,可以三件事一起做,也可以轮流做,也可能C需要最后做(等A.B做完),这三件事是相关的,抽象起来有三种场景. 顺序做 先买菜,再买肉,最后洗米, 即 A->B->C. 并发做 买菜,买肉,洗米,一起做. 交集做 买菜,买肉必须先做完,才能做洗米. 场景就是这样,接下来就是如何考虑用js实现. function A(callback){ setTimeout(function(){ c

利用ajaxfileupload.js异步上传文件

1.引入ajaxfileupload.js 2.html代码 <input type="file" id="enclosure" name="enclosure"> <button id="upClick" >上传</button> 注意这里的input控件的id和name必须一致:这样在后台利用springMVC接受文件的时候能对应起来: 3.JS代码 <script type=&q

Node.js异步处理CPU密集型任务

Node.js擅长数据密集型实时(data-intensive real-time)交互的应用场景.然而数据密集型实时应用程序并不是只有I/O密集型任务,当碰到CPU密集型任务时,比如要对数据加解密(node.bcrypt.js),数据压缩和解压(node-tar),或者要根据用户的身份对图片做些个性化处理,在这些场景下,主线程致力于做复杂的CPU计算,IO请求队列中的任务就被阻塞. Node.js主线程的event loop在处理所有的任务/事件时,都是沿着事件队列顺序执行的,所以在其中任何一