• 003-Web Worker工作线程


    一、关于Web Worker工作线程

      HTML5几个优势特性里,就包括了Web Worker,这货可以了解为多线程,正常形况下,浏览器执行某段程序的时候会阻塞直到运行结束后在恢复到正常状态,而HTML5的Web Worker就是为了解决这个问题。

      允许JavaScript创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。
      所以它能解决两个问题:一、解决程序阻塞问题;二、提升效率。

    二、示例

      常测试效率最常用的无非就是fibonacci了,我们也来个fibonacci性能测试。

    var start = (new Date()).getTime();
    var fibonacci =function(n) {
        return n<2 ? n : arguments.callee(n-1) + arguments.callee(n-2);
    };
    fibonacci(38);
    console.log((new Date()).getTime() - start);
    //console.log打出 4792

      每台电脑性能不一样,浏览器不一样可能会有变化,38执行需要接近5s,两次就差不多10s。

        fibonacci(38);
        fibonacci(38);
        // 两次执行需要 9694

      我们引入Web Worker,提升效率,缩短运算时差

    worker.js

    var fibonacci =function(n) {
        return n<2 ? n : arguments.callee(n-1) + arguments.callee(n-2);
    };
     
    self.addEventListener('message', function(event) {
        self.postMessage(fibonacci(event.data))
    }, false);

    主页面中的js

    //第二种
    var start = (new Date()).getTime(); 
    // 实例化一个worker实例,参数必须是一篇JavaScript脚本
    var worker = new Worker('worker.js'); 
    // 监听worker通信
    worker.addEventListener('message', function(event) {
        console.log('Worker result: ' + ((new Date()).getTime() - start));
    }, false);
      
    // 向worker post数据
    worker.postMessage(38);
      
    var fibonacci =function(n) {
        return n<2 ? n : arguments.callee(n-1) + arguments.callee(n-2);
    };
     
    // 主页面仅剩一个,另外一个已经转移到worker里执行了
    setTimeout(function(){
        fibonacci(38);
        //console.log((new Date()).getTime() - start);
        alert((new Date()).getTime() - start)
    }, 100);

    这时候的执行结果相当于执行两次用了一次的时间。

    三、封装

    但如果要连续执行好几个,可不能这样:

    worker.postMessage(38);
    worker.postMessage(38);
    worker.postMessage(38);

    因为只是new了一个Worker,所以它会顺序执行:

    script.js:26 5369
    script.js:9 Worker result: 5374
    script.js:9 Worker result: 9960
    script.js:9 Worker result: 14557

    封装:

    我们可以同时new多个

    //第三种
    var start = (new Date()).getTime(); 
    var fibonacci =function(n) { 
        // 实例化一个worker实例,参数必须是一篇JavaScript脚本
        var worker = new Worker('worker.js'); 
        // 监听worker通信
        worker.addEventListener('message', function(event) {
            console.log('Worker result: ' + ((new Date()).getTime() - start));
        }, false); 
        // 向worker post数据
        worker.postMessage(n);
    };
    fibonacci(38)
    fibonacci(38)
    fibonacci(38)
    fibonacci(38)

    执行4次的结果:

    script.js:11 Worker result: 9323
    script.js:11 Worker result: 9335
    script.js:11 Worker result: 9340
    script.js:11 Worker result: 9350
    可见实例越多,单个执行效率就越高,因为new一个Worker也是需要耗费时间的,但即使这样也比在浏览器里阻塞顺序执行效率更高。

    四、跨域与脚本引入

    1、Worker引入必须是本域

    Worker在实例化的时候必须要传入一个脚本URL,而且必须是在本域下,否则会报跨域错误:本域:http://localhost:63342/

    var worker = new Worker('http://localhost/worker.js');

    报安全错误:
      Uncaught SecurityError: Failed to construct 'Worker': Script at 'http://localhost/worker.js' cannot be accessed from origin 'http://localhost:63342'./

    2、Worker中代码通过importScripts方法引入任何域下的脚本

      可以在Worker里通过importScripts方法引入任何域下的脚本,就如同HTML里的script标签一样

      worker里引入它

        self.importScripts('http://localhost/script.js');

        console.log: Hello world! from http://localhost/script.js

    3、同步异步

      new Worker 与 importScripts是异步的

      importScripts加载是同步的

        self.importScripts('/script1');
        self.importScripts('/script2');

    五、优缺点

    5.1、支持api,在worker线程中,可以获得下列对象

    1)     navigator对象

    2)     location对象,只读

    3)     XMLHttpRequest对象,能发出Ajax请求

    4)     setTimeout/setInterval方法

    5)     Application Cache

    6)     通过importScripts()方法加载其他脚本,在worker里载入外部js脚本

    7)     创建新的Web Worker

    8)   addEventListener/postMessage,有了它们才能与主页互相通信【代码中可以直接使用onmessage

    5.2、worker线程不能获得下列对象

    1)     DOM对象

    2)     window对象

    3)     document对象

    4)     parent对象

    上述的规范,限制了在worker线程中获得主线程页面相关对象的能力,所以在worker线程中,不能进行dom元素的更新。

    5.3、用途

    1)  Web Worker带来后台计算能力

    更新数据和对象状态”的耗时部分交由Web Worker执行,提升页面性能。

    2)  使用专用线程进行数学运算

    Web Worker最简单的应用就是用来做后台计算,而这种计算并不会中断前台用户的操作

    3)  图像处理

    通过使用从<canvas>或者<video>元素中获取的数据,可以把图像分割成几个不同的区域并且把它们推送给并行的不同Workers来做计算

    4)  大量数据的检索

    当需要在调用 ajax后处理大量的数据,如果处理这些数据所需的时间长短非常重要,可以在Web Worker中来做这些,避免冻结UI线程。

    5)  背景数据分析

    Angular1最被大家诟病就是它的脏检查机制,当scope的数据量过多时会严重影响性能。而Angular2正是借助WebWorker来把繁重的计算工作移入辅助线程,让界面线程不受影响。

    六、原理流程

    1、分类

      Web Worker 规范中定义了两类工作线程,分别是专用线程Dedicated Worker和共享线程 Shared Worker,其中,Dedicated Worker只能为一个页面所使用,而Shared Worker则可以被多个页面所共享,本文示例为专用线程Dedicated Worker。

    2、详细说明

    使用Dedicated Worker的主页面代码main.js

    var worker = new Worker("task.js");
    worker.postMessage(
            {
                id:1,
                msg:'Hello World'
            }
    );
    worker.onmessage=function(message){
        var data = message.data;
        console.log(JSON.stringify(data));
        worker.terminate();
    };
    worker.onerror=function(error){
        console.log(error.filename,error.lineno,error.message);
    }

    Dedicated Worker所执行的代码task.js

    onmessage = function(message){
        var data=message.data;
        data.msg = 'Hi from task.js';
        postMessage(data);
    }

    在main.js代码中,首先通过调用构造函数,传入了worker脚本文件名,新建了一个worker对象,这一对象是新创建的工作线程在主线程的引用。随后调用worker.postMessage()方法,与新创建的工作线程通信,这里传入了一个json对象。随后分别定义了worker对象的onmessage事件和onerror事件的回调处理函数,当woker线程返回数据时,onmessage回调函数执行,数据封装在message参数的data属性中,调用 worker 的 terminate()方法可以终止worker线程的运行;当worker线程执行出错时,onerror回调函数执行,error参数中封装了错误对象的文件名、出错行号和具体错误信息。

     在task.js代码中,定义了onmessage事件处理函数,由主线程传入的数据,封装在message对象的data属性中,数据处理完成后,通过postMessage方法完成与主线程通信。在工作线程代码中,onmessage事件和postMessage方法在其全局作用域可以访问。

    2、执行流程

      

    1)     worker线程的创建的是异步的

    代码执行到"var worker = new Worker(task.js')“时,在内核中构造WebCore::JSWorker对象(JSBbindings层)以及对应的WebCore::Worker对象(WebCore模块),根据初始化的url地址"task.js"发起异步加载的流程;主线程代码不会阻塞在这里等待worker线程去加载、执行指定的脚本文件,而是会立即向下继续执行后面代码。

    2)     postMessage消息交互由内核调度

    main.js中,在创建woker线程后,立即调用了postMessage方法传递了数据,在worker线程还没创建完成时,main.js中发出的消息,会先存储在一个临时消息队列中,当异步创建worker线程完成,临时消息队列中的消息数据复制到woker对应的WorkerRunLoop的消息队列中,worker线程开始处理消息。在经过一轮消息来回后,继续通信时, 这个时候因为worker线程已经创建,所以消息会直接添加到WorkerRunLoop的消息队列中;

    1.3 worker线程数据通讯方式

    主线程与子线程数据通信方式有多种,通信内容,可以是文本,也可以是对象。需要注意的是,这种通信是拷贝关系,即是传值而不是地址,子线程对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给子线程,后者再将它还原。

    主线程与子线程之间也可以交换二进制数据,比如File、Blob、ArrayBuffer等对象,也可以在线程之间发送。但是,用拷贝方式发送二进制数据,会造成性能问题。比如,主线程向子线程发送一个50MB文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript允许主线程把二进制数据直接转移给子线程,转移后主线程无法再使用这些数据,这是为了防止出现多个线程同时修改数据的问题,这种转移数据的方法,叫做Transferable Objects。

    // Create a 32MB "file" and fill it.
    var uInt8Array = new Uint8Array(1024*1024*32); // 32MB
    for (var i = 0; i < uInt8Array .length; ++i) {
        uInt8Array[i] = i;
    }
    worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);

    七、使用woker的几个tips

    (1)使用多少个worker?

    确定要使用 Web Worker 之后,确定多少个 Worker 同时工作就是下面需要考虑的问题,数量少了发挥不出并行处理的优势,数量多了有可能导致 Worker 处理速度变慢。

    一般的做法是读取 navigator.hardwareConcurrency 这个属性,它表示机器最多可并行执行的任务数量,如果取不到这个值,可以给一个默认值,例如4。还有一种动态检测 Worker 数量的方法,有兴趣的话可以看:https://github.com/oftn-oswg/core-estimator 

    有人可能会问,假设一个机器的最大并行数是8,那么是不是只能创建7个 Worker,留一个给“主线程”使用?我的建议是要看的你的应用的实际情况,通常来说页面的“主线程”不会长时间执行操作,大部分时间都处于空闲状态,那么这时候 Worker 数量完全可以取8。通过 Chrome Dev Tools 的 Timeline(新版叫做 Performance)工具可以查看每个 Worker 的工作情况,确定是否影响“主线程”工作。

     

    (2)、优化woker与主线程通信开销 

    Worker 与“主线程”之间的数据传递默认是通过结构化克隆(Structured Clone)完成的。数据量较大时,克隆过程会比较耗时,这会影响 postMessage 和 onmessage 函数的执行时间。

    解决方案一、

      是先通过 JSON.stringify 将对象序列化,接收之后再用 JSON.parse 还原。因为:stringfiy + 传递字符串的耗时 < 传递对象的耗时 。 

    // 操作像素
        var imageData = context.createImageData(img.width, img.height);
        var work = new Worker('./cal.js');
        var data = {
            data: imageData.data,
             imageData.width,
            height: imageData.height
        };
        // 将传递的参数转换成字符串
        work.postMessage(JSON.stringify(data));

    解决方案二、

      避开克隆传值的方法,就是使用Transferable Objects,主要是采用二进制的存储方式,采用地址引用,解决数据交换的实时性问题;Transferable Objects支持的常用数据类型有ArrayBuffer和ImageBitmap; 

       // 操作像素
        var imageData = context.createImageData(img.width, img.height);
        var work = new Worker('./cal.js');
        // 转化为类型数组进行传递
        var int8s = new Int8Array(imageData.data);
        var data = {
            data: int8s,
             imageData.width,
            height: imageData.height
        };
        // 在postMessage方法的第二个参数中指定transferList
        work.postMessage(data, [data.data.buffer]);

    经测试,使用arrayBuffer之后,传递数据所需的时间为1ms,极大地提高了数据传输的效率。

    参看地址:

    https://yq.aliyun.com/ziliao/25009

    https://www.cnblogs.com/GongQi/p/4991380.html

    http://mp.weixin.qq.com/s/DORzY-Rgts7RAcfrbUeiDg

  • 相关阅读:
    Thank you for your resubmission. Performance
    node.js
    CSS
    2016第三本《曾国藩的正面和侧面》
    2016下第二本《启示录-产品篇》
    2016下第2本《启示录 -流程篇》
    2016下第2本《启示录 -人员篇》
    《点石成金-访客至上的web和移动可用性设计秘籍》读书笔记
    互联网二三事
    互联网二三事
  • 原文地址:https://www.cnblogs.com/bjlhx/p/8474599.html
Copyright © 2020-2023  润新知