• Web worker


    https://www.html5rocks.com/en/tutorials/workers/basics/

    JS的并发问题

      这里有一系列的瓶颈阻止JS客户端程序变得更好,如浏览器兼容性,可访问性和性能。幸运的是这些都已经成为过去,浏览器开发商提升了JS引擎的速度。

      但仍然有一个障碍,那就是JS语言本身是单线程的,意味着多个脚本不能同时运行。例如,很常见是一个站点需要处理UI事件,查询和处理大量的来自服务器的数据,还要操作dom。不幸的是,浏览器由于JS运行环境的限制 ,没办法同时执行这些操作。脚本只能运行在单线程中。

      开发者使用事件机制API(setTimeout、xhr、setInterval等)来模拟“并发”,这些API是异步的。好消息是相比这些hack,H5给我们提供了更好的东西。

    Web worker:给JS引入线程

      Web worker定义了一些API来后台运行脚本。允许我们执行一些长期运行的任务,却不会阻塞UI或者其他用于与用户进行交互的脚本。

      多个worker使用类似于线程间的消息通信来实现并行

    Web worker的类型

      说明书(spec)中指出有两种类型:Dedicated Worker 与 Shared Worker。后续仅仅介绍 Dedicated Worker(后续简称为 DW)。

    开始

      DW运行在一个隔离的线程,运行的代码一般在一个独立的文件中,以下创建一个DW

    var worker = new Worker('task.js');

      如果指定的文件存在,则浏览器会创建一个DW线程,文件下载是异步的,如果文件返回404,则执行失败但不会报错(fail sliently)

      以上创建完了一个DW之后,浏览器会异步下载脚本,下载完成后马上执行里面的代码(运行在线程中了),然后等待外部的postMessage(参数可以为空),worker内再执行内部回调函数中的代码:

    worker.postMessage(); // Start the worker.

     与worker通信

      父页面通过worker进行通信使用postMessage传递消息。参数可支持字符串或者JSON对象。以下例子往worker中传递一个hello world,而worker中把数据回传回来:

    // main
    var worker = new Worker('doWork.js');
    
    worker.addEventListener('message', function(e) {
      console.log('Worker said: ', e.data);
    }, false);
    
    worker.postMessage('Hello World'); // Send data to our worker.
    
    //doWorker.js
    self.addEventListener('message', function(e) {
      self.postMessage(e.data);
    }, false);

      通过worker.addEventListener('message',xx)或者worker.onmessage=xx来接收对方传来的消息,数据从evt.data中获取。

      传递的数据都是值传递,而不是共享。如以下例子中,传递的JS对象实际上时经过序列化传递的,另一端再进行反序列化,也就是说每次传递都会在另一端重新创建一个一模一样的JS对象。

    <button onclick="sayHI()">Say HI</button>
    <button onclick="unknownCmd()">Send unknown command</button>
    <button onclick="stop()">Stop worker</button>
    <output id="result"></output>
    
    <script>
      function sayHI() {
        worker.postMessage({'cmd': 'start', 'msg': 'Hi'});
      }
      function stop() {
        // worker.terminate() from this script would also stop the worker.
        worker.postMessage({'cmd': 'stop', 'msg': 'Bye'});
      }
      function unknownCmd() {
        worker.postMessage({'cmd': 'foobard', 'msg': '???'});
      }
      var worker = new Worker('doWork2.js');
      worker.addEventListener('message', function(e) {
        document.getElementById('result').textContent = e.data;
      }, false);
    </script>

       doWorker2.js

    self.addEventListener('message', function(e) {
      var data = e.data;
      switch (data.cmd) {
        case 'start':
          self.postMessage('WORKER STARTED: ' + data.msg);
          break;
        case 'stop':
          self.postMessage('WORKER STOPPED: ' + data.msg +
                           '. (buttons will no longer work)');
          self.close(); // Terminates the worker.
          break;
        default:
          self.postMessage('Unknown command: ' + data.msg);
      };
    }, false);

       停用worker有两种方式:worker内部调用close或者main中调用terminate方法。worker被关闭后,main中继续给他传递消息,不报错也不反应。

    关于结构化克隆算法(The structured clone algorithm)

      这是h5中的算法,用于复制复杂的JS对象。当通过postMessage函数与worker进行通信或者使用IndexedDB来保存对象时,内部就会用到这个算法。

      克隆的方式:递归地处理输入对象,同时维持一个关于之前访问的对象的map,防止进入死循环。

    性能

      chrome13 和FF5 支持使用ArrayBuffer(或者Typed Array)与Worker进行通信。如(给我的感觉就是普通传递了一个数组)

    // worker
    self.onmessage = function(e) {
      var uInt8Array = e.data;
      postMessage("Inside worker.js: uInt8Array.toString() = " + uInt8Array.toString());
      postMessage("Inside worker.js: uInt8Array.byteLength = " + uInt8Array.byteLength);
    };
    
    //main
    var uInt8Array = new Uint8Array(new ArrayBuffer(10));
    for (var i = 0; i < uInt8Array.length; ++i) {
      uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
    }
    
    console.log('uInt8Array.toString() = ' + uInt8Array.toString());
    console.log('uInt8Array.byteLength = ' + uInt8Array.byteLength);
    
    worker.postMessage(uInt8Array);

      浏览器不使用序列化和反序列化,而是使用结构化克隆算法来复制ArrayBuffer,这使父页面与worker可以使用二进制数据来进行通信。

      最开始传递的数据是通过序列化和反序列化成一个JSON,这要求数据符合JSON格式,所以像File、Blo和ArrayBuffer这些类型就没办法序列化了。后来’浏览器实现了结构化克隆,允许我们传递复杂的数据(非JSON键值对对象),如File、Blob、ArrayBuffer和JSON对象,但传递这些类型数据时,依然会重新创建一个对象,这是很消耗性能的(传递一个大于32M的ArrayBuffer耗时超过几百毫秒),为了解决这个问题,需要我们使用transferable Objects(后续简称为TO)。

      TO对象是一个拥有了transferable标志(tag)的对象,ArrayBuffer、MessagePort和ImageBitmap都属于是TO类型。postMessage方法可用于传递TO对象。

      新版本的浏览器使用postMessage传递TO时,有很大的性能提升:数据从一个上下文传递到另一个,不会重新创建对象。但是数据传递完成后,源上下文中的对应数据的引用会被清除。

      通过postMessage传递TO对象,需要使用新的postMessage签名:第一个参数是数据(可以使JSON对象也可以是ArrayBuffer),第二个参数要求必须是ArrayBuffer数组(传输的item的列表),如:

    worker.postMessage({data: int8View, moreData: anotherBuffer},
                       [int8View.buffer, anotherBuffer]);

    检测浏览器是否支持transferable

    var ab = new ArrayBuffer(1);
    worker.postMessage(ab, [ab]);
    if (ab.byteLength) {
      alert('Transferables are not supported in your browser!');
    } else {
      // Transferables are supported.
    }

     worker的运行环境

       可以使用self和this来应用当前worker的全局作用域。也就是说最开始的worker2.js可以这样写了:

    addEventListener('message', function(e) {
      var data = e.data;
      switch (data.cmd) {
        case 'start':
          postMessage('WORKER STARTED: ' + data.msg);
          break;
        case 'stop':
      ...
    }, false);
    
    onmessage = function(e) {
      var data = e.data;
      ...
    };

    worker内可以使用的JS特性

      因为多线程的原因,可以访问的JS特性有:navigator、location(只读)、xhr、timer API、appCache,通过importScript来引入外部脚本、生成其他worker。worker不能访问的有:dom(线程不安全),window对象,document对象,parent对象。

    worker内引入外部脚本

      内部直接使用importScripts方法,参数可以接收多个外部脚本。

    worker内创建子worker

      随着多核CPU的流行,获取更好运行性能的方法是把一个大任务分解成多个worker任务。子worker文件必须与父页面同源,文件路径相对于父worker。例子:

      主worker中创建子worker,给每个子worker指定一个start和end,每个子worker完成任务后主worker通过storeResult把结果传递到父页面去。

    // settings
    var num_workers = 10;
    var items_per_worker = 1000000;
    
    // start the workers
    var result = 0;
    var pending_workers = num_workers;
    for (var i = 0; i < num_workers; i += 1) {
      var worker = new Worker('core.js');
      worker.postMessage(i * items_per_worker);
      worker.postMessage((i+1) * items_per_worker);
      worker.onmessage = storeResult;
    }
    
    // handle the results
    function storeResult(event) {
      result += 1*event.data;
      pending_workers -= 1;
      if (pending_workers <= 0)
        postMessage(result); // finished!
    }

      子worker根据start和end开始工作,然后把结果传递回去,最后关闭自己

    var start;
    onmessage = getStart;
    function getStart(event) {
      start = 1*event.data;
      onmessage = getEnd;
    }
    
    var end;
    function getEnd(event) {
      end = 1*event.data;
      onmessage = null;
      work();
    }
    
    function work() {
      var result = 0;
      for (var i = start; i < end; i += 1) {
        // perform some complex calculation here
        result += 1;
      }
      postMessage(result);
      close();
    }

    内联的worker

      以上创建的worker都是基于一个独立的文件。其他也可以内联,要使用Blob:

    var blob = new Blob(["onmessage = function(e) { postMessage('msg from worker'); }"]);
    
    // Obtain a blob URL reference to our worker 'file'.
    var blobURL = window.URL.createObjectURL(blob);
    
    var worker = new Worker(blobURL);
    worker.onmessage = function(e) {
      // e.data == 'msg from worker'
    };
    worker.postMessage(); // Start the worker.

      以上worker的代码通过一个字符串数组传递给Blob,createObjectURL可以创建一个url来引用Blob或者File,返回的url大致如下(疑问为什么这里不会出现跨域错误?但worker内部如果通过importScript引用了相对路径的外部脚本,则提示跨域错误):

    blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1

      这种url都是唯一的,并且一直有效直到当前页面的文档被卸载。也可以手动释放这个url,使其马上变得无效,调用:

    window.URL.revokeObjectURL(blobURL);

      在chrome中,访问chrome://blob-internals/.可以看到所有被创建的blob url。

      以上使用Blob来实现内联worker,但是代码写在字符串中,感觉不方便。最好是转换一下思路,那就是使用script标签来获取字符串:

      <script id="worker1" type="javascript/worker">
        // This script won't be parsed by JS engines
        // because its type is javascript/worker.
        self.onmessage = function(e) {
          self.postMessage('msg from worker');
        };
        // Rest of your worker code goes here.
      </script>
    
      <script>
        function log(msg) {
          // Use a fragment: browser will only render/reflow once.
          var fragment = document.createDocumentFragment();
          fragment.appendChild(document.createTextNode(msg));
          fragment.appendChild(document.createElement('br'));
    
          document.querySelector("#log").appendChild(fragment);
        }
    
        var blob = new Blob([document.querySelector('#worker1').textContent]);
    
        var worker = new Worker(window.URL.createObjectURL(blob));
        worker.onmessage = function(e) {
          log("Received: " + e.data);
        }
        worker.postMessage(); // Start the worker.
      </script>

      使用内联的worker时要注意,如果worker内通过importScript来引用外部脚本,要求必须是绝对路径。如果使用了相对路径则报跨域错误。解决办法就是外部传入一个当前的绝对地址,worker内部手动将相对地址拼接为绝对地址

    ...
    <script id="worker2" type="javascript/worker">
    self.onmessage = function(e) {
      var data = e.data;
    
      if (data.url) {
        var url = data.url.href;
        var index = url.indexOf('index.html');
        if (index != -1) {
          url = url.substring(0, index);
        }
        importScripts(url + 'engine.js');
      }
      ...
    };
    </script>
    <script>
      var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
      worker.postMessage({url: document.location});
    </script>

    错误处理

      当一个worker正在运行时,内部出现了错误,则外部可以通过worker.addEventListener('error',xx)或者worker.onerror=xx来注册一个错误处理函数,evt中包含了3有有用的信息:

    1. filename:发生错误的worker脚本
    2. lineno:错误行号
    3. message:错误描述

      以下是一个例子:

    <output id="error" style="color: red;"></output>
    <output id="result"></output>
    
    <script>
      function onError(e) {
        document.getElementById('error').textContent = [
          'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
        ].join('');
      }
    
      function onMsg(e) {
        document.getElementById('result').textContent = e.data;
      }
    
      var worker = new Worker('workerWithError.js');
      worker.addEventListener('message', onMsg, false);
      worker.addEventListener('error', onError, false);
      worker.postMessage(); // Start worker without a message.
    </script>
    
    // worker
    self.addEventListener('message', function(e) {
      postMessage(1/x); // Intentional error.
    };

    安全

      由于chrome的安全限制,worker不能本地运行(file://),否则worker会fial silently。仅当开发测试的时候,可以禁用chrome这种限制,以 --allow-file-access-from-files 标志来运行chrome。

       所有worker脚本必须与当前页面同源。

    应用场景

    1. 预获取或者数据缓存,以便于后续使用
    2. 代码语法高亮或者其他实时文本编辑的效果处理
    3. 语法检查
    4. 分析视频或音频数据
    5. 在后台对服务器的轮训或者IO
    6. 处理大数组或者极大的JSON数据
    7. canvas中的图片滤镜处理
    8. 大量处理web数据库
  • 相关阅读:
    字符串编码之一:字符串语法
    PHP进阶学习系列1:字符串编码提纲
    关于技术成长的一些反思
    Yii2学习笔记002---Yii2的控制器和视图
    PHP5.3--PHP7 新特性总结
    计算机软考笔记之《数据库基础》
    计算机软考笔记之《文件结构》
    计算机软考笔记之《抽象数据类型(ADT)》
    计算机软考笔记之《数据压缩》
    计算机软考笔记之《数据结构与算法》
  • 原文地址:https://www.cnblogs.com/hellohello/p/8343387.html
Copyright © 2020-2023  润新知