JavaScript中并行性的快速介绍

251 阅读5分钟

JavaScript中并行性的快速介绍

并行主义在JavaScript中?你可能会想,我们已经在使用setTimeout()setInterval()XMLHttpRequestasync/await 和事件处理程序在JavaScript中进行并行编程了。但这并不是真的。作为开发者,我们一直在模仿并行编程,因为JavaScript的single threading nature 使用事件循环

是的,以上所有的技术都是异步的和非阻塞的。但是,这并不一定意味着并行编程。JavaScript的异步事件是在当前执行的脚本屈服后处理的。

满足 "网络工作者 "的要求

WebWorker为JavaScript引入了真正的并行编程。

Web Worker允许你启动长期运行的计算成本较高的任务,这有助于已经很拥挤的主线程将所有时间都用于布局和绘画😃。

理解渲染器流程中的Web Worker

Web Worker渲染器进程中的位置(chrome中的一个进程,它负责标签内发生的一切)。

renderer

每个chrome标签都有自己的渲染器进程,chrome也会尝试为具有相同域的标签提供一个渲染器进程,如果你想知道同一个渲染器进程是如何共享一个v8实例的,祝你好运!

渲染器进程会创建多个线程:

  • 主线程:处理大部分发送到浏览器的JavaScript
  • 工作线程:处理Web工作者
  • 合成器和光栅线程:负责渲染页面的流畅和高效

为了好玩,你可以通过以下方式检查chrome中运行的进程(不仅仅是渲染器进程)。

Menu(3 verticle dots on top right) > More tools > Task Manager

如何使用?

所以,只要我们做了

const worker = new Worker("worker.js")

就会创建新的工作线程,我们在worker.js 中的代码就会在其中运行。

与 Worker 进行通信

你不能直接调用工人线程中的函数。主线程和工作者之间的通信是通过via sending and listening to message event 完成的。

示例。寻找一个字符串的长度。

index.js

// message event helps to communicate between threads.

// to listen for incoming messages from the worker.
worker.addEventListener('message', e => {
  console.log(`Message from worker ${e.data}`)
})
// to send message to the worker
worker.postMessage("Hello from main")

worker.js

// to listen for incoming messages from the main thread or other worker.
// Yes, a worker can also create subworkers.
self.addEventListener('message', e => {
  console.log(`Message from main thread ${e.data}`)
  // send a message back to the main thread
  self.postMessage(e.data.length)
})

这里,postMessage需要一个参数message ,这个参数将在交付的事件的数据键中可用,消息的值应该由结构化克隆算法支持。

你也可以使用第二个参数来发送可转移对象的数组:

worker.postMessage(message, [transfer]);

工作者范围

Worker 的selfthis 都引用了 Worker 的全局范围。

根据前面的例子,self.postMessages(e.data.length) 也可以写成postMessages(e.data.length)

限制

由于工作者线程的多线程行为和线程安全,它不具备主线程所具有的一些功能:

  • DOM
  • 窗口对象
  • 文档对象
  • 父对象

参见MDN中的其他可用函数和类。

真实世界的例子

说得够多了,我们来写代码吧。

我们将编写一个小程序,使用Web工作者完成一些紧张的任务:接受一张图片并应用过滤器。

在终端运行

$ mkdir filter

$ touch index.html script.js worker.js

Html设置:

<!--index.html -->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Worker</title>
  </head>
  <body>
    
    <!-- Input to select filter -->
    <p>
      <label>
        Choose a filter to apply
        <select id="filter">
          <option value="none">none</option>
          <option value="grayscale">grayscale</option>
          <option value="brighten">brighten by 20%</option>
          <option value="threshold">threshold</option>
        </select>
      </label>
    </p>

    <!-- Get image -->
    <form accept-charset="utf-8" action="asd">
      <label>Image to filter</label>
      <input type="file" name="image" id="image-to-filter" alt="Enter image to filter"> 
    </form>

     <!-- show output here -->
    <canvas id="output"></canvas>

    <script src="script.js"></script>
  </body>
</html>

脚本设置:

//script.js

document.addEventListener('DOMContentLoaded', () => {

  const inputImage = document.getElementById('image-to-filter');
  const output  = document.getElementById('output');
  const filter = document.querySelector('#filter');

  // Context let you draw on the canvas
  const outputContext = output.getContext('2d');

  // This will return HTMLImageElement (<img>)
  const img = new Image();

  let imageData;

  const drawImg = () => {
    output.height = img.height;
    output.width = img.width;

    outputContext.drawImage(img, 0, 0);

    // returns ImageData object, we can get pixel data from ImageData.data 
    // https://developer.mozilla.org/en-US/docs/Web/API/ImageData/data
    imageData = outputContext.getImageData(0, 0, img.height, img.width);
  }

  // Getting image from the user
  inputImage.addEventListener('change', e => {
    
    // API to read files from the user
    const reader = new FileReader();

    // will be called once we read some file using any of the reader 
    // function available for FileReader
    reader.onload = e => {
      img.src = e.target.result;
      img.style.display = 'none';

      // will be called once the src is downloaded
      img.onload = () => {
        document.body.appendChild(img);
        drawImg();
      };
    };

     // reads the file as a dataURL
    reader.readAsDataURL(e.target.files[0]);
  });

})

好了,这是怎么回事呢?这主要是实现细节,即我们如何从用户那里获得图像,并将其值存储在imageData

这将创建一个工作者来运行worker.js 中的代码:

// script.js

const worker = new Worker('worker.js');

worker.js 中设置一个工作者。

如果你像我一样,你的第一直觉是,让我们传递一个函数来回调给worker,让它结束,但是,你不能传递一个函数给web worker,因为结构化克隆算法不支持函数:

// worker.js

const Filters = {};

Filters.none = function none() {};

Filters.grayscale = ({data: d}) => {
  for (let i = 0; i < d.length; i += 4) {
    const [r, g, b] = [d[i], d[i + 1], d[i + 2]];

    // CIE luminance for the RGB
    // The human eye is bad at seeing red and blue, so we de-emphasize them.
    d[i] = d[i + 1] = d[i + 2] = 0.2126 * r + 0.7152 * g + 0.0722 * b;
  }
};

Filters.brighten = ({data: d}) => {
  for (let i = 0; i < d.length; ++i) {
    d[i] *= 1.2;
  }
};

Filters.threshold = ({data: d}) => {
  for (var i = 0; i < d.length; i += 4) {
    var r = d[i];
    var g = d[i + 1];
    var b = d[i + 2];
    var v = 0.2126 * r + 0.7152 * g + 0.0722 * b >= 90 ? 255 : 0;
    d[i] = d[i + 1] = d[i + 2] = v;
  }
};

onmessage = e => {
  const {imageData, filter} = e.data;
  Filters[filter](imageData);
  postMessage(imageData);
};
  • 我们正在监听一个消息事件
  • 获取图像数据
  • 筛选图像数据
  • 将imageData发回给主线程。

向工作者发送一条消息:

// script.js

const filter = document.querySelector('#filter');
let imageData;

filter.addEventListener('change', e => sendImageDataToWorker())

const sendImageDataToWorker = () => { 
  worker.postMessage({imageData, filter: filter.value})
}

在这里,我们正在监听过滤器的变化,然后将 imageData 和要使用的过滤器发送给 Worker。

监听来自worker

//script.js

worker.onmessage = () => e => outputContext.putImageData(e.data, 0, 0);

我们正在监听来自工作者的消息,然后改变上下文中的图像数据。

最后,script.js ,将

// script.js

document.addEventListener('DOMContentLoaded', () => {
  const worker = new Worker('filter_worker.js');

  const inputImage = document.getElementById('image-to-filter');
  const outputC = document.getElementById('output');
  const filter = document.querySelector('#filter');
  const oCtx = outputC.getContext('2d');
  const img = new Image();
  let imageData;

  const sendDataToWorker = () =>
    worker.postMessage({imageData, filter: filter.value});

  const receiveFromWorker = e => oCtx.putImageData(e.data, 0, 0); 

  worker.onmessage = receiveFromWorker;

  const drawImg = () => {
    outputC.height = img.height;
    outputC.width = img.width;

    console.log(img.height);
    oCtx.drawImage(img, 0, 0); 
    imageData = oCtx.getImageData(0, 0, img.height, img.width);
    sendDataToWorker();
  };  

  inputImage.addEventListener('change', e => {
    const file = e.target.files[0];

    const reader = new FileReader();
    reader.onload = e => {
      img.src = e.target.result;
      img.style.display = 'none';
      img.onload = () => {
        document.body.appendChild(img);
        drawImg();
      };  
    };  

    reader.readAsDataURL(file);
  }); 

  filter.addEventListener('change', e => sendDataToWorker());
});

终止 Worker

您可以从主线程终止 Worker,方法是调用worker.terminate()

worker.terminate()

如果您想从工作者本身终止,可以调用工作者的close() 函数


close()

调用close() 时,事件循环中存在的任何排队任务都会被丢弃,Web Worker 的范围会被关闭。网络工作者也没有时间进行清理,因此突然终止工作者可能会导致内存泄漏。

其他网络工作者

在现实中,有两种类型的网络工作者。专用型共享型,在本篇文章的范围内,我们只使用专用型工作者。

使用案例

  • 与WebAssembly互动
  • 语法高亮
  • 图像过滤
  • 音频/视频分析
  • 处理大量的数组(对于数组,你可以使用二进制数组,这样复制起来更快)。