WebWorker学习-尝试将请求抽离主线程

2,680 阅读6分钟

WEB-WORKER 实用场景

  • 密集型计算任务 (递归、计算、数据处理)
  • 预处理请求
  • 高频请求(心跳检测、用户行为捕捉等)

内部实现队列机制

  • 初衷

    • 一般做用户行为分析都会埋入锚点,当用户点击后通过AJAX向后台发送用户的操作、时间、功能等信息

    • 在WEB-WORKER之前通过主线程发送高频的数据势必会占用主线程、并发请求上限。这就会造成请求的等待或者阻塞。这种情况下会通过跨域将数据发送到非同源后端服务,规避同源策略下的请求数量上限。但是主线程仍然会受到一定的影响

    • 所以这时候就考虑使用WEB-WORKER。仍然是规避同源策略。由于WEB-WORKER独立于主线程所以主线程将数据交由WORKER处理即可。不再需要支付额外的AJAX开销

    • 但是这就带来一个问题。当大量的WORK发送大量的AJAX那WORKER的请求就会等待,这样效率不高。所以就考虑在WORKER内部实现一个任务队列,按照压入的顺序执行,这样就可以使性能得到保证。

    • 115757_04ed5517_513857.png

    • 想要实现队列,必须要有对应的容器。在后端语言中有QUEUE可以直接使用。JAVASCRIPT中没有,但是我们有数组

      • 但是需要注意的是数组提供的POP函数是删除并返回数组的最后一个元素。队列是遵循先进先出。所以不能使用PUSH数组尾端添加元素,需要使用UNSHIFT向数组头部添加数据

    • 内部执行机制如下图

      • 1.通过监听接收主线程发送来的消息
      • 2.直接将消息压入队列中
      • 3.通过标记判断队列是否空闲
      • 4.队列空闲执行逻辑方法,队列非空闲则不做任何操作
      • 5.执行内部逻辑,执行完毕后返回数据,再次执行上一步。直到队列空闲
      • 6.更新队列空闲标记,等待下次消息到达
    • 121914_62db1263_513857.png

    • 这样保证了任务的顺序执行,同时也解决了高并发请求等待的问题

    • 122312_b891b784_513857.png

关于线程池

  • 初衷
    • 在使用WEB-WORKER的过程中,一般要手动开启WEB-WORKER。然后向WORKER发送消息执行逻辑接收返回参数。
    • 出于平衡资源与不浪费性能的考虑,将WORKER的能力集中化。也就是将代码都编写在一个WORKER中。通过轮询的方式均衡每个WORKER。达到很好的复用的效果
    • 所以这就有了线程池的概念。线程池一次开启多个子线程提供使用。内部提供集中负载算法,均衡子线程的承载。常见的有轮询,加权轮询,最佳可用,随机等。
    • 线程池提供统一的执行入口。子线程通过参数判断执行的任务类型做对应逻辑。通过数组装载子线程实例。Map做子线程任务索引,int类型的轮询计数
      • 轮询 内部持有递增变量通过 变量%数组长度 (loop % pool.length)
      • 最佳可用 计算线程的等待数 * 平均耗时 选择结果是最小值的子线程 (min(waot * rtt))
    • 133418_45e666d3_513857.png

性能分析

  • 火狐性能分析
    182104_d65598d5_513857.png
  • 单线程线性队列
    110056_d45ac47f_513857.png
  • 线程池并发任务
    110107_61e17b6e_513857.png

相关代码

    /**
     * 多线程工具
     */
    thread: {

        /** 轮询计数 */
        _loop: 0,

        /** 线程池 {id:String, wait:int, thraad: webWorker} */
        _pool: [],

        /** 任务Map, key为每个任务的唯一ID, value存储任务的回调函数等信息 */
        taskMap: new Map(),

        /** 均衡策略 */
        BALANCE_RULE: {
            /** 轮询 */
            ROUND_ROBIN: 0,
            /** 最佳可用, 选择等待排队最短的队列 */
            BEST_AVAILABLE: 1,
            /** 随机, 应该不太会用到 */
            RANDOM: 99
        },

        /** 均衡算法 */
        BALANCE_ALGORITHM: {
            /** @return {number} 轮询下标 */
            ROUND_ROBIN: function () {
                return this._loop++ % this._pool.length;
            },
            /** @return {number} 等待最少的任务下标 // TODO SHADOW 待优化,子线程RTT由哪段处理 */
            BEST_AVAILABLE: function () {
                var min, index = 0;
                this._pool.forEach(function (thread, i) {
                    var wait = thread.wait;
                    if (typeof min === "undefined") {
                        min = wait;
                        index = i;
                    } else {
                        if (wait < min) {
                            min = wait;
                            index = i;
                        }
                    }
                });
                return index;
            },
            /** @return {number} 随机下标 */
            RANDOM: function () {
                return Math.round(Math.random() * (this._pool.length - 1))
            }
        },

        /** 负载均衡 */
        balanceRule: 0,

        /**
         * 默认回调函数
         * @param event 线程回到
         * @private
         */
        _defaultCallback: function (event) {
            var result = event.data;
            // 校验返回数据是否存在
            if (result.id) {
                this._updateThreadPool(result);
                this._reactive(result)
            } else {
                throw new Error("线程响应没有ID");
            }
        },

        /**
         * 响应方法
         * 通过ID 获取任务Map中的回调,然后调用
         * @param result 线程返回结果
         * @private
         */
        _reactive: function (result) {
            var task = this.taskMap.get(result.id);
            if (task && task.callback) {
                task.callback.call(undefined, result.body)
            }
            this.taskMap.delete(result.id);
        },

        /**
         * 更新线程池信息
         * @param task 线程返回结果
         * @private
         */
        _updateThreadPool: function (task) {
            this._pool.forEach(function (thread) {
                if (thread.id === task.threadId) {
                    thread.wait = task.wait
                }
            })
        },

        /**
         * 初始化 - 单个线程
         * @param src 脚本路径
         * @param autoClose 是否自动关闭 默认true
         * @returns {Worker}
         */
        single: function (src, autoClose) {
            if (!Worker) {
                throw new Error("浏览器不支持Web Worker")
            }
            var scriptRegexp = /(\S*).js$/g;
            // 校验js文件的正确性
            if (!src || !scriptRegexp.test(src)) {
                throw new Error("脚本文件路径不正确!");
            }
            // 校验文件是否存在
            autoClose = autoClose !== false;
            var thread = new Worker(base + src);
            var the = this;
            thread.onmessage = function (event) {
                the._defaultCallback(event);
                if (autoClose) {
                    the.terminate(thread);
                }
            };
            thread.onerror = function (ev) {
                throw new Error("线程创建失败!");
            };
            return thread;
        },

        /**
         * 初始化 - 线程池
         * @param src 脚本路径
         */
        pool: function (src) {
            var poolSize = navigator.hardwareConcurrency && navigator.hardwareConcurrency > 2 ? navigator.hardwareConcurrency : 2;
            var index = 0;
            while (index < poolSize) {
                this._pool.push({
                    id: this.getUniqueID("THREAD_"),
                    wait: 0,
                    worker: this.single(src, false),
                    closed: false
                });
                index++;
            }
        },

        /**
         * 执行任务,调用Worker.postMessage 向worker线程发送数据
         * @param options 参数
         * @param callback 回调
         * @param thread 线程
         */
        run: function (options, callback, thread) {
            if (!thread) {
                // 如果没有传入线程,向线程池获取资源
                thread = this.getWorker();
                if (!thread) {
                    throw new Error("没有传入执行线程并且也没有在线程池中找到可用线程!")
                }
            }
            if (thread.closed) {
                throw new Error("该线程已关闭/释放资源!")
            }
            if (!options) {
                throw new Error("没有接收到任何参数!");
            }
            var id = this.getUniqueID("TASK_");
            var message = {id: id, body: options, threadId: thread.id};
            var task = {id: id, body: options, callback: callback};
            this.taskMap.set(task.id, task);
            thread instanceof Worker ? thread.postMessage(message) : thread.worker.postMessage(message);
        },

        /**
         * 获取线程池资源
         * 线程池使用机制,轮询,空闲优先,加权算法
         * @returns {Worker}
         */
        getWorker: function () {
            var index;
            switch(this.balanceRule) {
                case this.BALANCE_RULE.ROUND_ROBIN: index = this.BALANCE_ALGORITHM.ROUND_ROBIN.call(this);break;
                case this.BALANCE_RULE.BEST_AVAILABLE: index = this.BALANCE_ALGORITHM.BEST_AVAILABLE.call(this);break;
                case this.BALANCE_RULE.RANDOM: index = this.BALANCE_ALGORITHM.RANDOM.call(this);break;
            }
            return this._pool[index];
        },

        /**
         * 关闭线程,调用Worker.terminate
         * @param thread 线程对象
         */
        terminate: function (thread) {
            if (thread instanceof Worker) {
                thread.terminate();
            } else {
                thread.worker.terminate();
            }
            thread.closed = true;
        },

        /**
         * 清除线程池
         */
        destroy: function () {
            if(this._pool.length > 0) {
                this._pool.forEach(this.terminate);
            }
        },

         /**
         * 唯一ID
         */
        getUniqueID: function (prefix) {
                return (typeof prefix == 'undefined' || "" == prefix) ? 'prefix_' + Math.floor(Math.random() * (new Date()).getTime()) : prefix + Math.floor(Math.random() * (new Date()).getTime());
        }    
    }
/**
 * 监听事件
 * @param event
 */
self.addEventListener("message", event => {
    RequestQueue.addTask(event.data);
});

/**
 * 请求队列
 * 1.脚本加载完成后启动
 * 2.接收数据后做基础校验,校验通过后压入队列.
 *
 */
const RequestQueue = function () {

    /** 内部队列*/
    const queue = [];

    /** 队列空闲标志 */
    let ready = true;

    /**
     * 添加任务
     * 如果队列空闲将任务压入队列,同时执行
     * 如果没有空闲则只将任务压入
     * @param data 数据
     */
    let addTask = function (data) {
        if (_validate(data)) {
            if(ready){
                queue.unshift(data);
                _run();
            }else {
                queue.unshift(data);
            }
        }
    };

    /**
     * 校验
     * @param data 数据
     * @returns {boolean} undefined/true
     * @private
     */
    let _validate = function (data) {
        if (!data.body) {
            throw new Error("传输数据不存在!")
        }
        return true;
    };

    /**
     * 运行任务
     * 1.将队列中的数据弹出
     * 2.如果有数据执行_execute执行
     * @private
     */
    let _run = function () {
        let next = queue.pop();
        if (next) {
            _execute(next);
            ready = false
        }else {
            ready = true
        }
    };

    /**
     * 执行
     * @param next 下一个数据
     * @private
     */
    let _execute = function (next) {
        const body = next.body;
        try {
            // dothing
            _run();
            postMessage({...});
        } catch (e) {
            _run();
            postMessage({...});
        }
    };

    return {
        addTask: addTask,
    }
}();