前端固有的编程思维是单线程,比如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的操作也有限制,只允许以下三种:

  1. 向外传送message;
  2. 根据接受到的message分发对应任务。Actor对于message对应的任务并没有限定为静态的,而是可以携带动态数据甚至函数,这样就大大地增强了Actor的可定制性;
  3. 创建其他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的工作包括以下几个:

  1. 管理一个worker线程,负责向worker线程发送message和接收message的实质行为;
  2. 维护一个私有任务队列,在线程被占用时将后续任务塞入队列,并且在线程空闲时自动取出队列中下个任务并执行;
  3. 维护一个私有状态-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。以一个典型的用户交互触发重绘的行为为例,整个流程如下:

  1. 用户操作地图改变地图视野(bound)之后会触发WebGL引擎的重绘行为;
  2. 第一步是通过当前视野计算可见的瓦片坐标列表,如果需要新的瓦片则触发加载;
  3. tile_pyramid.ts调用分发器dispatcher.ts执行加载瓦片的任务;
  4. dispatcher.ts首先会判断所有Actor中是否有被占用的,如果存在空闲Actor则直接将任务分配给它,如果没有空闲Actor则随机选择一个Actor执行任务,此时被选中的Actor会将任务塞入任务队列,排队执行。

总结

以上便是WebGL引擎的对于Actor+worker的具体实现模式,加入负载均衡概念之后可以更有效地解决线程被占用时的任务动态分配。因为此WebGL引擎是内部项目,不便将更细节的代码写出来,比如worker的具体任务,所以大家就将就看吧。

使用Actor模型管理Web Worker多线程的更多相关文章

  1. 一个简单的HTML5 Web Worker 多线程与线程池应用

    笔者最近对项目进行优化,顺带就改了些东西,先把请求方式优化了,使用到了web worker.发现目前还没有太多对web worker实际使用进行介绍使用的文章,大多是一些API类的讲解,除了涉及到一些 ...

  2. Web Worker 多线程

    Web Workers多线程 1  浏览器把所有事件都通过操作系统安排到事件队列中(例如:你去一个·窗口买菜,需要排队):浏览器使用单线程处理队列中的事件和执行用户代码(也就是单线程:web work ...

  3. 深入HTML5 Web Worker应用实践:多线程编程

    HTML5 中工作线程(Web Worker)简介 至 2008 年 W3C 制定出第一个 HTML5 草案开始,HTML5 承载了越来越多崭新的特性和功能.它不但强化了 Web 系统或网页的表现性能 ...

  4. 深入 HTML5 Web Worker 应用实践:多线程编程

    深入 HTML5 Web Worker 应用实践:多线程编程 HTML5 中工作线程(Web Worker)简介 至 2008 年 W3C 制定出第一个 HTML5 草案开始,HTML5 承载了越来越 ...

  5. JS线程模型&amp;Web Worker

    js线程模型 客户端javascript是单线程,浏览器无法同时运行两个事件处理程序 设计为单线程的理论是,客户端的javascript函数必须不能运行太长时间,否则会导致web浏览器无法对用户输入做 ...

  6. Web Worker javascript多线程编程(一)

    什么是Web Worker? web worker 是运行在后台的 JavaScript,不占用浏览器自身线程,独立于其他脚本,可以提高应用的总体性能,并且提升用户体验. 一般来说Javascript ...

  7. Web Worker javascript多线程编程(二)

    Web Worker javascript多线程编程(一)中提到有两种Web Worker:专用线程dedicated web worker,以及共享线程shared web worker.不过主要讲 ...

  8. web Worker使js实现‘多线程’?

    大家都知道js是单线程的,在上一段js执行结束之前,后面的js绝对不会执行,那么为什么标题说js实现‘多线程’,虽然说加了引号,可是标题也不能乱写不是,可恶的标题党? 姑且抛开标题不说,先说我们经常会 ...

  9. javascript 多线程Web Worker不引用外部js文件的方法

    最近在Android开发中 Webview通过调用JavascriptInterface的方式与App交互 在交互的过程中,有些App上的操作时间会比较长,Web中调用的话会造成程序假死的情况 于是想 ...

  10. 深入理解javascript异步编程障眼法&amp;&amp;h5 web worker实现多线程

    0.从一道题说起 var t = true; setTimeout(function(){ t = false; }, 1000); while(t){ } alert('end'); 1 2 3 4 ...

随机推荐

  1. linux基础学习

    1.默认不写端口号的就是80端口 本地ip:localhost或者127.0.0.1 2.用户管理 id和whoami:可以查看当前用户 who和w查看当前已经登录的用户 (1)添加用户,用户默认的家 ...

  2. C# .NET 隐藏窗体

    隐藏窗体,打开窗体后如果想让它隐藏,然后再显示出来,就判断是不是NULL或者有没有关闭,不然就NEW一个出来,否则就SHOW出来. 当然如果有隐藏的话退出的时候最好用Application.Exit( ...

  3. sql server 常用的函数小汇

    摘录些许sqlserver 常用到的一些函数,便于日常学习使用 一.字符转换函数1.ASCII()返回字符表达式最左端字符的ASCII 码值.在ASCII()函数中,纯数字的字符串可不用‘’括起来,但 ...

  4. 用 CSS 实现字符串截断

    [要求]:如何用css实现字符串截断,超出约定长度后用缩略符...代替?   ♪ 答: <html> <head> <meta charset="UTF-8&q ...

  5. iOS开发—— UIImagePickerController获取相册和拍照

    一.简单的拍照显示,或是从相册中直接选取照片 #import "ViewController.h" @interface ViewController ()<UIImageP ...

  6. 团队Github实战训练

    班级:软件工程1916|W 作业:团队Github实战训练 团队名称:SkyReach Github地址:Github地址 贡献比例表 队员学号 队员姓名 此次活动任务 贡献比例 221600106 ...

  7. Linux_CentOS-服务器搭建 &lt;一&gt;

    本人CentOS版本6.3 必备的两个小软件: 安装PUTTY远程控制linux的非常小但非常好用的小工具. 安装WINSCP,使用ssh实现我windows上和linux服务器上文件的互传. 呵呵, ...

  8. ASP项目部署IIS7.5中遇到的问题

    我们大家都熟悉了tomcat服务器的部署,如果是一个ASP项目如何部署呢.这也是我在客户现场遇到的问题.ASP项目一般是用的系统组件IIS来部署项目.下面我讲一下自己在部署过程中遇到的问题. 如果在网 ...

  9. [CQOI2009]叶子的染色

    传送门:https://www.luogu.org/problemnew/show/P3155 一道挺水的树形dp题,然后我因为一个挺智障的问题debug了一晚上…… 嗯……首先想,如果一个点的颜色和 ...

  10. oracle 查询 约束

    select * FROM all_constraints where CONSTRAINT_NAME='SYS_xxx'