[译] 探索Web Workers在Web多线程方面的潜力

avatar

原文链接

多线程是现代软件开发中用于提高应用程序性能和响应能力的重要技术。然而,由于 JavaScript 的单线程特性,在Web上使用多线程并不常见。为了克服这个限制,引入了Web Workers作为在Web应用程序中实现多线程的一种方式。在本文中,Sarah Oke Okolo(作者) 探讨了Web Workers在Web上进行多线程处理的重要性,包括使用它们的限制和考虑因素,以及缓解与Web Workers相关的潜在问题的策略。

Web Workers是现代Web开发的一个强大功能,它们作为HTML5规范的一部分于2009年引入。它们被设计为一种在后台执行JavaScript代码的方式,与网页的主执行线程分离,以提高性能和响应能力。

主线程是负责呈现UI、执行JavaScript代码和处理用户交互的单个执行上下文。换句话说,JavaScript是“单线程”的。这意味着任何耗时的任务,比如复杂的计算或数据处理,如果在主线程上执行,会阻塞主线程,导致用户界面冻结和无响应。

而这些,正是 Web Workers 发挥作用的地方。

Web Workers 被实现为解决这个问题的一种方式,它允许耗时的任务在一个称为工作者线程的独立线程中执行。这使得 JavaScript 代码能够在后台执行,而不会阻塞主线程并导致页面无响应。

在 JavaScript 中创建一个 Web Worker 并不是一项复杂的任务。以下步骤提供了将 Web Worker 集成到您的应用程序中的起点:

  1. 创建一个新的 JavaScript 文件,其中包含您要在worker线程中运行的代码。该文件不应包含对 DOM 的任何引用,因为它将无法访问 DOM。

  2. 在您的主 JavaScript 文件中,使用 Worker 构造函数创建一个新的 worker 对象。该构造函数接受一个参数,即您在步骤 1 中创建的 JavaScript 文件的 URL。

    const worker = new Worker('worker.js');
  1. 给 worker 对象添加事件监听器,以处理主线程和worker线程之间发送的消息。使用 onmessage 事件处理程序来处理从worker线程发送的消息,而使用 postMessage 方法来向worker线程发送消息。
    worker.onmessage = function(event) {
        console.log('Worker said: ' + event.data);
    };
    worker.postMessage('Hello, worker!');
  1. 在您的 worker.js 文件中,通过使用 self 对象的 onmessage 属性,添加一个事件监听器来处理从主线程发送的消息。您可以使用 event.data 属性访问随消息一起发送的数据。
    self.onmessage = function(event) {
        console.log('Main thread said: ' + event.data);
        self.postMessage('Hello, main thread!');
    };

现在让我们运行Web应用程序并测试worker。我们应该看到打印到控制台的消息,表明在主线程和辅助线程之间发送和接收了消息。

1-messages-console-between-main-worker-threads.png

Web Workers和主线程之间的一个关键区别是,Web Workers无法访问DOM或用户界面(UI)。这意味着它们无法直接操作页面上的HTML元素或与用户进行交互。「 Web Workers被设计用于执行不需要直接访问UI的任务,例如数据处理、图像操作或计算。 」

另一个重要的区别是,Web Workers被设计为在与主线程分离的沙箱环境中运行,这意味着它们对系统资源的访问受到限制,无法访问某些API,例如localStorage或sessionStorage API。然而,它们可以通过消息传递系统与主线程进行通信,允许在两个线程之间交换数据。

Web Workers 在 Web 上进行多线程处理的重要性和好处

Web Workers为Web开发人员提供了一种在Web上实现多线程的方法,这对于构建高性能Web应用程序至关重要。通过使耗时的任务能够在后台执行,与主线程分开,Web Workers提高了网页的整体响应能力,并允许更无缝的用户体验。以下是Web Workers在Web上多线程处理的一些重要性和好处。

提高资源利用率

通过允许耗时任务在后台执行,Web Workers更有效地利用系统资源,实现数据更快、更高效的处理,从而提高整体性能。这对于涉及大量数据处理或图像操作的Web应用程序尤为重要,因为Web Workers可以在不影响用户界面的情况下执行这些任务。

提高稳定性和可靠性

通过将耗时任务隔离在单独的工作线程中,Web Workers有助于防止在主线程上执行大量代码时可能发生的崩溃和错误。这使得开发人员更容易编写稳定可靠的Web应用程序,降低用户遇到烦恼或数据丢失的可能性。

增强安全性

Web Workers在一个与主线程分离的沙箱环境中运行,这有助于增强Web应用程序的安全性。这种隔离性可以防止恶意代码访问或修改主线程或其他Web Workers中的数据,从而降低数据泄露或其他安全漏洞的风险。

更好的资源利用率

Web Workers可以通过释放主线程来处理用户输入和其他任务,同时让Web Workers在后台处理耗时的计算,从而帮助改善资源利用率。这有助于提高整体系统性能,并降低崩溃或错误的可能性。此外,通过利用多个CPU核心,Web Workers可以更有效地利用系统资源,实现数据更快、更高效的处理。

Web Workers还可以实现Web应用程序的更好负载均衡和扩展。通过允许任务在多个工作线程上并行执行,Web Workers可以将工作负载均匀分布在多个核心或处理器上,实现更快、更高效的数据处理。这对于经受高流量或需求的Web应用程序尤为重要,因为Web Workers可以确保应用程序能够处理增加的负载而不影响性能。

Web Workers的实际应用

让我们探索一下Web Workers的一些常见和有用的应用。无论您是构建复杂的Web应用程序还是简单的网站,了解如何利用Web Workers可以帮助您提高性能并提供更好的用户体验。

将CPU密集型工作转移到其他资源

假设我们有一个需要执行大规模CPU密集型计算的Web应用程序。如果我们在主线程中执行这个计算,用户界面会变得不响应,用户体验会受到影响。为了避免这种情况,我们可以使用Web Worker在后台执行计算。

// Create a new Web Worker.
const worker = new Worker('worker.js');

// Define a function to handle messages from the worker.
worker.onmessage = function(event) {
  const result = event.data;
  console.log(result);
};

// Send a message to the worker to start the computation.
worker.postMessage({ num: 1000000 });

// In worker.js:

// Define a function to perform the computation.
function compute(num) {
  let sum = 0;
  for (let i = 0; i < num; i++) {
    sum += i;
  }
  return sum;
}

// Define a function to handle messages from the main thread.
onmessage = function(event) {
  const num = event.data.num;
  const result = compute(num);
  postMessage(result);
};

在这个例子中,我们创建了一个新的Web Worker,并定义了一个处理来自Worker的消息的函数。然后,我们向Worker发送一个带有参数(num)的消息,该参数指定计算中要执行的迭代次数。Worker接收到这个消息后,在后台执行计算。当计算完成后,Worker将结果发送回主线程的消息。主线程接收到这个消息后,将结果记录到控制台。

2-console-number-message-main-thread.png

这个任务涉及将从0到给定数字之间的所有数字相加。尽管对于小数来说,这个任务相对简单明了,但对于非常大的数字来说,它可能会变得计算密集型。

在上面的示例代码中,我们将数字1000000传递给了Web Worker中的compute()函数。这意味着compute函数将需要将从0到一百万的所有数字相加。这涉及大量的附加操作,并且可能需要相当长的时间才能完成,特别是如果代码运行在较慢的计算机上或在浏览器标签中已经有其他任务在执行。

通过将这个任务转移到Web Worker,应用程序的主线程可以继续平稳运行,而不会被计算密集型任务阻塞。这使得用户界面保持响应,并确保其他任务(如用户输入或动画)能够在没有延迟的情况下处理。

处理网络请求

让我们考虑这样一个场景:一个Web应用程序需要发起大量的网络请求。在主线程中执行这些请求可能会导致用户界面不响应,从而给用户带来糟糕的体验。为了避免这个问题,我们可以利用Web Workers在后台处理这些请求。通过这样做,主线程保持空闲以执行其他任务,而Web Worker同时处理网络请求,从而提高性能和提供更好的用户体验。


// Create a new Web Worker.
const worker = new Worker('worker.js');

// Define a function to handle messages from the worker.
worker.onmessage = function(event) {
  const response = event.data;
  console.log(response);
};

// Send a message to the worker to start the requests.
worker.postMessage({ urls: ['https://api.example.com/foo', 'https://api.example.com/bar'] });

// In worker.js:

// Define a function to handle network requests.
function request(url) {
  return fetch(url).then(response => response.json());
}

// Define a function to handle messages from the main thread.
onmessage = async function(event) {
  const urls = event.data.urls;
  const results = await Promise.all(urls.map(request));
  postMessage(results);
};

并行处理

假设我们有一个需要执行大量独立计算的Web应用程序。如果我们在主线程中按顺序执行这些计算,用户界面将变得不响应,用户体验将受到影响。为了避免这种情况,我们可以使用Web Worker并行执行这些计算。


// Create a new Web Worker.
const worker = new Worker('worker.js');

// Define a function to handle messages from the worker.
worker.onmessage = function(event) {
  const result = event.data;
  console.log(result);
};

// Send a message to the worker to start the computations.
worker.postMessage({ nums: [1000000, 2000000, 3000000] });

// In worker.js:

// Define a function to perform a single computation.
function compute(num) {
  let sum = 0;
  for (let i = 0; i < num; i++) {
    sum += i;
}
  return sum;
}

// Define a function to handle messages from the main thread.
onmessage = function(event) {
  const nums = event.data.nums;
  const results = nums.map(compute);
  postMessage(results);
};


在这个例子中,我们创建一个新的Web Worker,并定义一个函数来处理来自Worker的消息。然后,我们向Worker发送一个带有要计算的数字数组的消息。Worker接收到这个消息后,使用map方法并行执行计算。当所有计算完成后,Worker将带有结果的消息发送回主线程。主线程接收到这个消息后,将结果记录到控制台。

限制和注意事项

Web Workers是提高Web应用程序性能和响应性的强大工具,但在使用它们时也需要考虑一些限制和注意事项。以下是一些最重要的考虑因素:

浏览器支持

Web Workers在所有主要的浏览器中都得到支持,包括Chrome、Firefox、Safari和Edge。然而,仍然有一些其他浏览器不支持Web Workers,或者可能有限的支持。

要更全面地了解浏览器的支持情况,请查看Can I Use网站。

在使用任何功能之前,请务必检查浏览器对该功能的支持情况,并进行全面的测试,以确保应用程序的兼容性。

限制访问DOM

Web Workers在一个独立的线程中运行,无法直接访问主线程中的DOM或其他全局对象。这意味着您无法直接从Web Worker中操作DOM或访问全局对象,如window或document。

为了解决这个限制,您可以使用postMessage方法与主线程进行通信,并通过消息来间接更新DOM或访问全局对象。例如,您可以使用postMessage方法将数据发送到主线程,然后根据接收到的消息来更新DOM或全局对象。

另外,一些库可以帮助解决这个问题。例如,WorkerDOM库可以让您在Web Worker中执行DOM操作,从而实现更快的页面渲染和改善性能。

通信开销

Web Workers通过postMessage方法与主线程进行通信,因此可能会引入通信开销。通信开销指的是建立和维持两个或多个计算系统之间通信所需的时间和资源。在Web应用程序中,例如在Web Worker和主线程之间的通信中,这可能导致消息处理的延迟,并潜在地降低应用程序的性能。为了减少这种开销,您应该只在线程之间发送必要的数据,并避免发送大量的数据或频繁的消息。

有限的调试工具

与调试主线程中的代码相比,调试Web Workers可能更具挑战性,因为可用的调试工具较少。为了使调试更容易,您可以使用控制台API在工作线程中记录消息,并使用浏览器开发者工具检查线程之间发送的消息。

代码复杂度

使用Web Workers可能会增加代码的复杂性,因为您需要管理线程之间的通信,并确保数据正确传递。这可能会使编写、调试和维护代码变得更加困难,因此您应该仔细考虑是否在您的应用程序中需要使用Web Workers。

缓解Web Workers潜在问题的策略

Web Workers是提高Web应用程序性能和响应性的强大工具。然而,使用Web Workers时可能会出现一些潜在问题。以下是一些缓解这些问题的策略:

使用消息批处理将通信开销降至最低

消息批处理涉及将多个消息组合成一个批处理消息,这比单独发送单个消息更高效。这种方法减少了主线程和Web Workers之间的往返次数。它有助于减少通信开销,提高Web应用程序的整体性能。

要实现消息批处理,您可以使用队列来积累消息,并在队列达到一定阈值或经过一段时间后将它们一起发送为批处理。下面是一个示例,展示了如何在您的 Web Worker 中实现消息批处理:

// Create a message queue to accumulate messages.
const messageQueue = [];

// Create a function to add messages to the queue.
function addToQueue(message) {
  messageQueue.push(message);
  
  // Check if the queue has reached the threshold size.
  if (messageQueue.length >= 10) {
    // If so, send the batched messages to the main thread.
    postMessage(messageQueue);
    
    // Clear the message queue.
    messageQueue.length = 0;
  }
}

// Add a message to the queue.
addToQueue({type: 'log', message: 'Hello, world!'});

// Add another message to the queue.
addToQueue({type: 'error', message: 'An error occurred.'});

在这个示例中,我们创建了一个消息队列来积累需要发送到主线程的消息。每当使用 addToQueue 函数将消息添加到队列时,我们会检查队列是否达到了阈值大小(在这个例子中为十个消息)。如果达到了阈值,我们就使用 postMessage 方法将批处理的消息发送到主线程。最后,我们清空消息队列,以准备下一批消息。

通过以这种方式进行消息批处理,我们可以减少主线程和 Web Worker 之间发送的消息总数.

避免同步方法

这些是阻塞其他代码执行直到完成的 JavaScript 函数或操作。同步方法可以阻塞主线程,导致应用程序无响应。为了避免这种情况,在 Web Worker 代码中应避免使用同步方法。相反,应使用异步方法,如 setTimeout() 或 setInterval(),来执行长时间运行的计算。

以下是一个小示例:

// In the worker
self.addEventListener('message', (event) => {
  if (event.data.action === 'start') {
    // Use a setTimeout to perform some computation asynchronously.
    setTimeout(() => {
      const result = doSomeComputation(event.data.data);

      // Send the result back to the main thread.
      self.postMessage({ action: 'result', data: result });
    }, 0);
  }
});

注意内存使用情况

Web Workers拥有自己的内存空间,这个空间的大小取决于用户设备和浏览器设置。为了避免内存问题,您应该注意Web Worker代码使用的内存量,并避免不必要地创建大型对象。例如:


// In the worker
self.addEventListener('message', (event) => {
  if (event.data.action === 'start') {
    // Use a for loop to process an array of data.
    const data = event.data.data;
    const result = [];

    for (let i = 0; i < data.length; i++) {
      // Process each item in the array and add the result to the result array.
      const itemResult = processItem(data[i]);
      result.push(itemResult);
    }

    // Send the result back to the main thread.
    self.postMessage({ action: 'result', data: result });
  }
});

在这段代码中,Web Worker处理了一个数据数组,并使用postMessage方法将结果返回给主线程。然而,用于处理数据的for循环可能会耗费很多时间。

这是因为代码一次性处理整个数据数组,这意味着所有数据必须同时加载到内存中。如果数据集非常大,这可能会导致Web Worker消耗大量内存,有可能超出浏览器为Web Worker分配的内存限制。

浏览器兼容性

Web Workers在大多数现代浏览器中得到支持,但是一些较旧的浏览器可能不支持它们。为了确保与各种浏览器的兼容性,您应该在不同的浏览器和版本中测试您的Web Worker代码。您还可以使用特性检测来检查是否支持Web Workers,然后再在代码中使用它们,例如:


if (typeof Worker !== 'undefined') {
  // Web Workers are supported.
  const worker = new Worker('worker.js');
} else {
  // Web Workers are not supported.
  console.log('Web Workers are not supported in this browser.');
}

这段代码检查当前浏览器是否支持Web Workers,并在支持时创建一个新的Web Worker。如果浏览器不支持Web Workers,则代码会在控制台上记录一条消息,提示浏览器不支持Web Workers。

通过遵循这些策略,您可以确保您的Web Worker代码高效、响应迅速,并且与广泛的浏览器兼容。

总结

随着Web应用程序变得越来越复杂和要求越来越高,高效的多线程技术(例如Web Workers)的重要性可能会增加。Web Workers是现代Web开发的重要功能,它允许开发人员将CPU密集型任务转移到单独的线程中,从而提高应用程序的性能和响应能力。然而,在使用Web Workers时,需要注意一些重要的限制和考虑因素,例如无法访问DOM以及在线程之间传递数据类型的限制。

为了缓解这些潜在问题,开发人员可以采用之前提到的策略,例如使用异步方法,并注意被转移任务的复杂性。

在未来,使用 Web Workers 进行多线程处理很可能仍然是提高 Web 应用性能和响应性的重要技术。虽然 JavaScript 还有其他实现多线程的技术,比如使用 WebSocketsSharedArrayBuffer,但 Web Workers 具有几个优势,使其成为开发人员强大的工具。

采用更现代的技术,如WebAssembly,可能会为使用Web Workers来卸载更复杂和计算密集型任务开辟新的机会。总体而言,Web Workers在未来几年中很可能会继续发展和改进,帮助开发人员创建更高效、响应更快的Web应用程序。

此外,存在许多库和工具可帮助开发人员使用Web Workers。例如,Comlink和Workerize提供了简化的API,用于与Web Workers进行通信。这些库抽象了一些管理Web Workers的复杂性,使得更容易利用它们的优势。

希望这篇文章能够让您对Web Workers在多线程方面的潜力以及如何在自己的代码中使用它们有一个良好的理解。