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数据库
posted @ 2018-01-24 20:22  HelloHello233  阅读(329)  评论(0编辑  收藏  举报