使用Actor模型管理Web Worker多线程
前端固有的编程思维是单线程,比如JavaScript语言的单线程、浏览器JS线程与UI线程互斥等等,Web Woker是HTML5新增的能力,为前端带来多线程能力。这篇文章简单记录一下搜狗地图WebGL引擎(下文简称WebGL引擎)使用Web Worker的一些实践方案,虽然这个项目最终夭折并且我也从搜狗离职了,但在开发WebGL引擎过程中的一些心得和实践还是值得写一写的。
搜狗地图WebGL引擎使用Actor模型管理worker线程,所以这篇文章就围绕这一点展开,包括以下内容:
- WebGL引擎为何要使用Web Worker以及对worker线程的需求定位
- Actor模型是什么以及为何它适用于Web Worker
- WebGL引擎的Actor模型+Web Worker的实践方案
WebGL引擎对Web Worker的需求定位
我们看到的电子地图实际上是由一个个网格拼合起来,这些网格叫做瓦片。根据瓦片的类型,地图可以分两种,一种是用静态图片配合css拼接,这种称为栅格地图;另一种是由WebGL将数据绘制为图形,这些数据便是真实的地理坐标,这种称为矢量地图。
这么说其实不太严谨,大多数电子地图使用的是墨卡托坐标,经过计算后转换为屏幕坐标,而不是真实的经纬度坐标,这个话题不属于本文的范畴,以后会单独讲
栅格地图是位图拼接的,是非矢量的,缩放会失真,这是缺点;优点是性能好,因为不需要很多计算。而矢量地图恰好相反,需要非常庞大的计算量,而优点便是缩放不会失真,并且可以实现3D效果。
传统的网站大多数用不到Web Worker或者对worker线程的要求比较轻,比如拉个数据啥的。Web Worker最佳的应用场景是计算密集类业务,而WebGL引擎在前端领域内可以说计算最密集的应用,体现在两方面:
- 数据量庞大
- 计算复杂且密集
比如下面这张图是Level 8的中国局部地图:
每个红色的网格就是一个瓦片,瓦片中的数据其实是一个个坐标点以及POI信息(坐标、文案等),WebGL引擎的工作包括以下几种:
- 根据当前视野计算瓦片坐标;
- 从后台接口获取瓦片数据;
- 渲染。
WebGL的渲染管线比较复杂,除了基本的GPU渲染管线以外,在CPU层面也有很繁重的工作,比如数据治理、缓存、创建纹理、矩阵计算等等。后面我会专门写一篇渲染管线的介绍。
看起来很简单,就跟「把大象关进冰箱」一样拢共分三步,但其实里面的逻辑和计算非常复杂,我会在后续的文章里一一剖析,这篇只挑选与worker线程相关的内容讲。Web Worker在其中的主要工作有以下几个:
从接口获取瓦片数据。这个比较简单,没啥好说的,说白了就是网络请求,稍微特殊的就是地图瓦片的数据比较大,请求耗时相对会长一点;
将瓦片数据解析为绘制可用的数据。瓦片数据可以简单理解为地理坐标+规则,WebGL引擎需要将地理坐标转化为屏幕坐标,然后按照规则将其进一步转化为最终可绘制的数据。这些规则包括样式(颜色/线宽等)、图形类别(Polygon/Line/Point等)、权重等,其中权重是比较特殊的一种规则,代表图形的绘制优先级,高优先级的后绘制,这是因为WebGL的绘制过程中,后绘制的图形会遮盖同位置已有的图形。
对POI进行定位计算。这个整个地图引擎中最复杂的一套计算流程。瓦片中的POI原始数据仅仅是一个点的地理坐标和文本,其中文本需要对应创建一个2D canvas作为WebGL的纹理。WebGL引擎首先需要从style文件中获取到POI的图标,然后将文本换算为canvas的尺寸,计算出整个POI图形的尺寸。比如天津的POI图形是这样的:
它最终的尺寸是包括坐标红点图标+坐标文本(实际是canvas纹理)的尺寸。而这类还算比较简单的POI,因为周边几乎没有其他POI,更复杂的还需要根据冲突检测结果动态调整文本与图标的相对位置,比如下图的两个POI,「微电子与纳电子学习」POI文本在图标的下方,『超导量子信息处理实验室』POI的文本就只能置于左侧、右侧或下方,否则会冲突。
最后一步是对视野内的所有POI进行冲突检测,剔除优先级低且位置与高优POI冲突的条目。这类计算在WebGIS业内有种通用的算法,叫做R树算法,JavaScript可用的开源工具推荐rbush。
对文本进行定位计算,比如道路的名称需要沿着道路线条布局如下图,这项工作量也比较复杂,后面我会单独写一篇。
综合以上的描述,WebGL对于worker线程的需求可以概括为两点:网络请求和计算。这两项工作交给worker线程之后,主线程便可以将资源集中在处理用户交互上,从而提高用户体验。
上面说的都是前提和需求,接下来就讲一讲如何实践的,首先介绍今天另一位主角:Actor模型。
Actor模型是什么
Actor模型是一个为了解决并行计算问题的抽象概念,它并不是一个新词,诞生在40多年之前。大致背景是因为单核CPU无法突破性能瓶颈只能通过多核并行计算提高效率,Actor模型就是为了解决并行计算由共享可变状态引起的race condition、dead lock等问题,更多细节自己去Wiki查。
在前端领域Actor模型并没有被广泛使用,因为在Web Worker出现之前,前端并没有并行计算的条件,Google在2018年的Chrome dev submit中介绍了使用Actor模型搭配Web Worker的一套简易架构,这才有更多前端开发者去关注Actor模型。
Actor模型有以下几个特点:
- 轻量:每个Actor只负责自己的工作,没有副作用;
- 没有共享状态:每个Actor的state都是private,不存在共享状态。理想情况下,每个Actor都运行在不同的线程,也不存在共享内存;
- 借助message通信:每个Actor通过接收message分发任务,可以理解为每个message都会触发一个任务,因此可能产生任务排队,每个Actor维护一个private task queue,每个task执行结束后通过message向外传递信息。
以上特点可以概括为下图所示的模型:
除了以上特点以外,Actor的操作也有限制,只允许以下三种:
- 向外传送message;
- 根据接受到的message分发对应任务。Actor对于message对应的任务并没有限定为静态的,而是可以携带动态数据甚至函数,这样就大大地增强了Actor的可定制性;
- 创建其他Actor。一个Actor对于它创建的其他Actor有管理员权限,可以定制其他Actor的某些行为。比如Actor A创建了Actor B,对于Actor B来说,Actor A就是Supervisor Actor。Actor A可以限制Actor B的行为,比如当Actor B崩溃以后发送一个message通知Actor A,这样Actor A就可以在接收到这个message时重启Actor B。这种机制跟PM2的重启机制很相似。通过这个特性也能看出来,Actor模型不仅适用于处理并行计算问题,同样适合分布式系统。
再说说为何Actor模型适合用来管理Web Worker线程。
前端使用Web Worker实现的多线程是一种主从(Master-Slave)模式:
- worker线程只具备有限的权限,不能操作DOM,从这个角度上来说,worker线程对于浏览器来说是线程安全的;
- worker线程与master线程(即JS主线程)之间通过postMessage通信;
- master线程通过发送message指定worker执行哪些行为,worker线程通过message返回结果。
Actor理论模型中并没有规定多线程使用哪种模式,但是Supervisor Actor的存在很适合主从多线程,所以与Web Worker的结合看上去非常合适。
但在实现层面,不一定完全遵从Actor理论模型,往往需要具体场景做一些改造,下面就简单讲一讲WebGL引擎在Actor+Web Worker方面的具体实现方式。
Actor模型在WebGL引擎渲染的实践应用
WebGL引擎对于worker线程的管理是一种类似负载均衡的模式,在Actor模型的基础之上增加了一个Dispatcher用于统筹管理所有的Actor,如下图:
每个Actor的工作包括以下几个:
- 管理一个worker线程,负责向worker线程发送message和接收message的实质行为;
- 维护一个私有任务队列,在线程被占用时将后续任务塞入队列,并且在线程空闲时自动取出队列中下个任务并执行;
- 维护一个私有状态-private busy,代表线程是否被占用,同时向外部提供访问入口public busy,Dispacher可以通过busy状态在所有Actor之间进行负载均衡。
Actor的伪代码如下:
export default class Actor {
private readonly _worker:Worker;
private readonly _id:number;
private _callbacks:KV<Function> = {};
private _counter: number = 0;
private _queue:MessageObject[]=[];
private _busy:boolean=false;
constructor(worker:Worker, id:number) {
this._id=id;
this._worker = worker;
this.receive = this.receive.bind(this);
this._worker.addEventListener('message', this.receive, false);
}
/**
* 占用状态
* @memberof Actor
*/
get busy():boolean{
return this._busy;
}
set busy(status:boolean){
this._busy = status;
// 解除占用状态后如果待执行队列不为空则执行队首任务
if(!status&&this._queue.length){
const {action,data,callback} = this._queue.shift();
this.send(action,data,callback);
}
}
/**
* @memberof Actor
*/
get worker():Worker{
return this._worker;
}
/**
* @private
* @method _postMessage
* @param message
*/
private _postMessage(message) {
this._worker.postMessage(message);
}
private _queueTask(action:WORKER_ACTION, data, callback?:Function){
this._queue.push({action,data,callback});
}
public receive(message:TypePostMessage) {
this.busy = false;
const {id,data} = message.data;
const callback = id?this._callbacks[id]:null;
callback&&callback(data);
delete this._callbacks[id];
}
public send(action:WORKER_ACTION, data, callback?:Function) {
if(this.busy){
this._queueTask(action,data,callback);
return;
}
this.busy = true;
const callbackId = `${this._id}-${action}-cb-${this._counter}`;
if(callback){
this._callbacks[callbackId] = callback;
this._counter++;
}
this._postMessage({
action,
data,
id: callbackId,
});
}
}
Dispatcher的工作比较简单,向上负责接收外层逻辑的调用命令,向下负责管理所有Actor的调度,代码如下:
export default class Dispatcher {
private readonly _actorsCount: number = 1;
private _actors: Actor[]=[];
constructor(count:number) {
this._actorsCount = count;
for (let i = 0; i < count; i++) {
this._actors.push(new Actor(new IWorker(''),i));
}
}
/**
* @public
* @method broadcast 广播指令
* @param {WORKER_ACTION} action 指令名称
* @param {Object} data 数据
*/
public broadcast(action: WORKER_ACTION, data: any) {
for(const actor of this._actors){
actor.send(action, data);
}
}
/**
* @public
* @method send 向单个worker发送动作指令
* @param {WORKER_ACTION} action 指令名称
* @param {Object} data 数据
* @param {Function} [callback] 回调函数
* @param {string} [workerId] 指定worker id
*/
public send(action:WORKER_ACTION, data: any, callback?:Function,workerId?:string) {
const actor = this._actors.filter(a=>!a.busy)[0];
if(actor){
actor.send(action, data, callback);
}else{
const randomId = Math.floor(Math.random()*this._actorsCount);
this._actors[randomId].send(action,data,callback);
}
}
/**
* @public
* @method clear 终止所有worker,清空actors
*/
public clear() {
for(const actor of this._actors){
actor.worker.terminate();
}
this._actors = [];
}
}
Dispatcher需要一个广播API,用来给所有Actor同步信息,比如将瓦片数据中的地理坐标转化为屏幕坐标需要用到屏幕的DPR,可以借助broadcast API将这个信息发送给所有Actor。
另外,Dispatcher并没有接受Actor的message,而是以回调函数的模式为每次任务分配一个handler,Actor执行完任务之后会触发对应的handler。以一个典型的用户交互触发重绘的行为为例,整个流程如下:
- 用户操作地图改变地图视野(bound)之后会触发WebGL引擎的重绘行为;
- 第一步是通过当前视野计算可见的瓦片坐标列表,如果需要新的瓦片则触发加载;
tile_pyramid.ts
调用分发器dispatcher.ts
执行加载瓦片的任务;dispatcher.ts
首先会判断所有Actor中是否有被占用的,如果存在空闲Actor则直接将任务分配给它,如果没有空闲Actor则随机选择一个Actor执行任务,此时被选中的Actor会将任务塞入任务队列,排队执行。
总结
以上便是WebGL引擎的对于Actor+worker的具体实现模式,加入负载均衡概念之后可以更有效地解决线程被占用时的任务动态分配。因为此WebGL引擎是内部项目,不便将更细节的代码写出来,比如worker的具体任务,所以大家就将就看吧。
使用Actor模型管理Web Worker多线程的更多相关文章
- 一个简单的HTML5 Web Worker 多线程与线程池应用
笔者最近对项目进行优化,顺带就改了些东西,先把请求方式优化了,使用到了web worker.发现目前还没有太多对web worker实际使用进行介绍使用的文章,大多是一些API类的讲解,除了涉及到一些 ...
- Web Worker 多线程
Web Workers多线程 1 浏览器把所有事件都通过操作系统安排到事件队列中(例如:你去一个·窗口买菜,需要排队):浏览器使用单线程处理队列中的事件和执行用户代码(也就是单线程:web work ...
- 深入HTML5 Web Worker应用实践:多线程编程
HTML5 中工作线程(Web Worker)简介 至 2008 年 W3C 制定出第一个 HTML5 草案开始,HTML5 承载了越来越多崭新的特性和功能.它不但强化了 Web 系统或网页的表现性能 ...
- 深入 HTML5 Web Worker 应用实践:多线程编程
深入 HTML5 Web Worker 应用实践:多线程编程 HTML5 中工作线程(Web Worker)简介 至 2008 年 W3C 制定出第一个 HTML5 草案开始,HTML5 承载了越来越 ...
- JS线程模型&Web Worker
js线程模型 客户端javascript是单线程,浏览器无法同时运行两个事件处理程序 设计为单线程的理论是,客户端的javascript函数必须不能运行太长时间,否则会导致web浏览器无法对用户输入做 ...
- Web Worker javascript多线程编程(一)
什么是Web Worker? web worker 是运行在后台的 JavaScript,不占用浏览器自身线程,独立于其他脚本,可以提高应用的总体性能,并且提升用户体验. 一般来说Javascript ...
- Web Worker javascript多线程编程(二)
Web Worker javascript多线程编程(一)中提到有两种Web Worker:专用线程dedicated web worker,以及共享线程shared web worker.不过主要讲 ...
- web Worker使js实现‘多线程’?
大家都知道js是单线程的,在上一段js执行结束之前,后面的js绝对不会执行,那么为什么标题说js实现‘多线程’,虽然说加了引号,可是标题也不能乱写不是,可恶的标题党? 姑且抛开标题不说,先说我们经常会 ...
- javascript 多线程Web Worker不引用外部js文件的方法
最近在Android开发中 Webview通过调用JavascriptInterface的方式与App交互 在交互的过程中,有些App上的操作时间会比较长,Web中调用的话会造成程序假死的情况 于是想 ...
随机推荐
- centos 下安装redis 通过shell脚本
#! /bin/bash echo -e "开始安装redis服务\n" download_url=http://download.redis.io/releases/redi ...
- ansible用user/group模块管理受控机上的用户和组(ansible2.9.5)
一,ansible的user/group模块的用途: ansible的user模块用来实现:生成用户.删除用户等用户的管理ansible的group模块用来实现:生成组.删除组等组的管理 说明:刘宏缔 ...
- JS 身份证号码验证
function checkIdcard(idcard) { var Errors = new Array( "验证通过!", "身份证号码位数不对!", &q ...
- C# 面试前的准备_基础知识点的回顾_04
1.Session和Cookie的使用区别 很容易回答的就是Session在服务器端,存储的数据可以较大容量,比如我们存一个Table,上千条数据. Cookie保存在客户端,安全系数低,不能放重要的 ...
- .gdbinit文件配置
.gdbinit文件配置 #打印数组的索引下标 set print array-indexes on #每行打印一个结构体成员 set print pretty on #除了断点有关的线程会被停下来, ...
- find for /f 分割字符串 bat
@Echo off::总用例数For /f "tokens=2" %%i in ('Type bat.txt^|Find "Ran"') do (Echo %% ...
- Jupyter Notebook使用教程
关于安装我就不说了,可以参考知乎https://zhuanlan.zhihu.com/p/33105153(总结的很全面) 首先打开Jupyter Notebook后,新建notebook:点击右上角 ...
- Graph-GraphSage
MPNN很好地概括了空域卷积的过程,但定义在这个框架下的所有模型都有一个共同的缺陷: 1. 卷积操作针对的对象是整张图,也就意味着要将所有结点放入内存/显存中,才能进行卷积操作.但对实际场景中的大规模 ...
- linux安装日志切割程序
====linux安装日志切割程序==== 安装 gcc(1) yum insatll gcc (2)# cd cronolog-1.6.2 4.运行安装 # ./configure# make# m ...
- 【转】Optimized Surface Loading and Soft Stretching
FROM:http://lazyfoo.net/tutorials/SDL/05_optimized_surface_loading_and_soft_stretching/index.php Opt ...