Skip to content

Web Worker -- Thread.js 多线程计算任务

TIP

以下示例展示的是多线程请求图片资源,而后做图片相似度匹配的部分逻辑。 图片比较工具使用的是 pixelmatch.js

一、依赖安装与配置

shell
# 安装threads node 和 浏览器可用
npm install threads tiny-worker
# 安装webpack插件。默认的worker.js 只支持es5,无法使用import 和 require。
npm install -D threads-plugin

vue.config.js

js
// vue.config.js
// 引入
const ThreadsPlugin = require('threads-plugin')
// 配置plugins

    configureWebpack: {
        resolve: {
            alias: {
                "@": resolve('src')
            }
        },
        plugins:[ new ThreadsPlugin()]
    }

二、业务逻辑

如果计算量很大,我们不可能无限制的开线程,因此需要使用 队列 + 线程池的思想。

由此,我们使用Thread.js。 https://threads.js.org/

main线程

js
// 在处理业务逻辑的页面加入如下代码

// mock data
const eventData = {
  img1: "https://www.baidu.com/202103/1efbc777522e4279926b9acc02d4d1a7.jpg",
  img2: "http://www.baidu.com/uca/UCA-023/600_c/f4f4f4.jpg",
  listId: "1-PABSFWRWER0123",
  threshold: 0.1,
}

// 【step1】 
// 左侧图片下载,转为blob。此操作只需要在每次点击row时执行。
// 创建worker对象
const workerFun = await spawn(new Worker("@/worker/ThreadWorkerMulti"))
// 调用workerjs中暴露的getImageBlob function()
const blob = await workerFun.getImageBlob(eventData.img1)
// 完成后关闭worker线程
await Thread.terminate(workerFun);

// 【step2】
// 右侧图片相似度对比与计算

// mock data
const rightSearchList =[];
rightSearchList.push(eventData);

// 实际业务
const myTasks= []
// 声明线程池
const pool = Pool(() => spawn(new Worker("@/worker/ThreadWorkerMulti")),{ concurrency:4,name:"task-img-diff"})
for (let i = 0; i < rightSearchList.length; i++) {
    // 模拟传参
    const task = pool.queue(worker =>  worker.doImgDiff(blob,eventData.img2,eventData.listId,eventData.threshold))
      .then()
      // error 输出
      .catch((error)=> {
    	console.log("error ==", error)
  	   })
       .finally()
    myTasks.push(task);
}

// 返回结果result list 
const result = await Promise.all(myTasks)
.finally(()=>{
  // 关闭线程池
  pool.completed();
  pool.terminate();
})
/**
 * 注意异常,undefined 的判断
 * 返回值data
 * [{
 *   listId: "1-PABSFWRWER0123",
 *   percent: 43.4
 * }]
 */  
console.log("pools result = ", result)

worker线程

js
// ThreadWorkerMulti.js
/* eslint-disable */
import {expose} from "threads"
import pixelmatch from 'pixelmatch'

/**
 *
 * @param tmpBlob 源图片blob
 * @param img2Url 要比对的图片url地址
 * @param listId  唯一标识id
 * @param threshold 系数
 */
async function doImgDiff(tmpBlob, img2Url, listId, threshold) {
    // 源图片,即左侧图片
    // const tmpBlob = await getImageBlob(img1Url)
    // 目标图片,即右侧分词查询list中的图片
    const tmpBlob2 = await getImageBlob(img2Url)
    const imageBitmap = await self.createImageBitmap(tmpBlob);
    const imageBitmap2 = await self.createImageBitmap(tmpBlob2);

    let canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
        // willReadFrequently == > Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true warnings
    let ctx = canvas.getContext('2d',{ willReadFrequently: true});
    const diff = ctx.createImageData(imageBitmap.width, imageBitmap.height)
    const imageData = getImageData(imageBitmap, ctx, canvas);
    const imageData2 = getImageData(imageBitmap2, ctx, canvas);

    const score = getImgScore(imageData, imageData2, diff, threshold, listId)
    return score;
}


/**
 * 获取图片blob二进制流
 * @param url
 * @returns {Promise<unknown>}
 */
function getImageBlob(url) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        url = "https://www.baidu.com/images?imageUrl=" + url
        xhr.open("get", url, true);
        xhr.responseType = "blob";
        xhr.onload = function () {
            if (this.status == 200) {
                resolve(this.response);
            } else {
                reject(new Error('error-'));
            }
        };
        xhr.send();
        xhr.onerror = () => {
            reject(new Error('error-'));
        }
    })
}

/**
 * 计算图片相似度
 * @param imageData1
 * @param imageData2
 * @param diff
 * @returns {number|number}
 */
function getImgScore(imageData1, imageData2, diff, _threshold, _listId) {
    const score = pixelmatch(imageData1.data, imageData2.data, diff.data, imageData1.width, imageData1.height, {threshold: _threshold})
    const percent = ~~(score / (imageData1.width * imageData1.height) * 1000) / 10
    const result = {
        listId: _listId,
        percent: percent
    }
    console.log("result ===", result)
    return result;
}

function getImageData(imageBitmap, ctx, canvas) {
    ctx.drawImage(imageBitmap, 0, 0, canvas.width, canvas.height)
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
    return imageData;
}

// 要导出的函数
const exposeFun={
    doImgDiff, getImageBlob
}

expose(exposeFun)