十三、并发编程

278 阅读6分钟

可读性:★★★★✰ 理解难度:★★★★✰

概述

对象是过程的抽象,线程是调度的抽象。

一、为什么要并发

并发是一种解耦策略,它帮助我们把做什么(目的)和何时做(时机)分解开。这样的策略能明显改进应用的吞吐量和结构。

二、并发编程三要素

要实现并发编程,必须保证以下3个要素,不然程序在并发的过程中就会存在不可预期的bug。

2.1 原子性

原子,一个不可再被分割的颗粒。原子性,指一个或多个不可分割的操作。

比如一个参数的读写操作必须是一个原子操作,不可以同时进行修改和读取操作。

我们可以用synchronized 或 Lock 来把读写操作“变成”原子操作。 简单来说,就是一个线程对变量修改时,其它线程暂时不能访问该变量。

2.2 可见性

当多个线程访问同一个变量时,其中一个线程对此变量作了修改,其他线程可以立刻读取到最新修改后的值。

int a = 0//线程A执行的代码
a = 10//线程B执行的代码
j = a

visual.png

2.3 有序性

程序的执行顺序按照代码的先后顺序来执行。

// 线程 1
init()
initDone = true// 线程2
while(initDone){
  executeAfterInit()
}

上面这段代码,如果按书写顺序执行,则不会有什么问题。

但是,如果线程1的initDone = true,在init()之前执行了。则会导致线程2的executeAfterInit方法出现不可预期的问题。

三、实现并发编程的方式

很多编程语言都有实现并发编程的方式,比如java:

  • 继承Thread类
  • 实现Runnable接口
  • 通过实现Callable接口
  • 通过线程池来实现多线程
  • 通过Timer定时器来实现多线程
  • jdk1.8通过stream实现多线程

四、Javascript 中的并发编程

这里进行一下概念的转换,并发是多个线程同时执行行,而不阻塞主线程。其实与Js中的异步任务很相似。那么前端有哪些实现异步的方式呢?

4.1 回调函数

const callback = () => {
    // do something
}
// 发起ajax请求,返回后执行回调
ajax(url, callback)

其中包括:事件监听、发布订阅等。

4.2 Promise

Promise规范有很多,包括Javascript’s Promises/A/B/D/A+,es6采用了Promises/A+的规范。

Promise对象有以下两个特点。

(1)Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。状态的改变是不可逆。

这里有一篇深入理解Promise的文章:从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节

4.3 Generator

generator即生成器,是ES6规范带来的新内容,在generator能够让我们在函数执行时任意地方暂停,在后续遇到合适的时机需要使用这个函数时继续执行。以往我们遇到的函数都是一口气执行到底,而generator的特点就是让函数执行到中间“刹车”,在需要它的时候接着执行。Generator运行是惰性的。(1)

function* foo() {
  yield 'result1'
  yield 'result2'
  yield 'result3'
}
  
const gen = foo()
console.log(gen.next()) // {value: "result1", done: false}
console.log(gen.next()) // {value: "result2", done: false}
console.log(gen.next()) // {value: "result3", done: false}
console.log(gen.next()) // {value: undefined, done: true}
Generator的作用
  1. 实现迭代器

    ES 标准虽然没有直接支持 Iterator API,但它通过了内建迭代器的方案。就是使用 Symbol.iterator,这是 ES6 引入的。

    实现了内建迭代器方法的对象称为可迭代对象String, Array, TypedArray, MapSet 是 Javascript 中内置的可迭代对象:

    const a = [1, 3, 5]
    const iter = a[Symbol.iterator]() // Array Iterator {}
    iter.next() // {value: 1, done: false}
    

    对象的扩展运算符(...)内部其实是调用了 Iterator 接口。

  2. Generator是协程在js上的实现。通过generator,我们可以在单线程的JavaScript里使用协程
  3. Generator可以用来模拟多线程的休眠机制
  4. generator本身作为异步编程的解决方案,可以用来解决异步任务,可配合co库使用
迭代器模式

迭代器模式(Iterator Pattern)又称为游标模式(Cursor Pattern),它提供一种顺序访问集合/容器对象元素的方法,而又无须暴露集合内部表示。迭代器模式可以为不同的容器提供一致的遍历行为,而不用关心容器内容元素组成结构,属于行为型模式。

应用:迭代器模式在我们生活中应用的也比较广泛,比如物流系统中的传送带,不管传送的是什么物品,都被打包成一个一个的箱子并且有一个统一的二维码。这样我们不需要关心箱子里面是啥,我们在分发时只需要一个一个检查发送的目的地即可。再比如,我们平时乘坐交通工具,都是统一刷卡或者刷脸进站,而不需要关心是男性还是女性,是残疾人还是正常人等个性化的信息。

设计模式-迭代器模式学习之旅

4.4 async/await

async 函数是 Generator 函数的语法糖。await意思是async await(异步等待)。这个关键字只能在使用async定义的函数里面使用。

async/await 让异步代码看起来、表现起来像同步代码,更加易于理解与阅读。

async函数对 Generator 函数的改进:

  1. 内置执行器,不需要调用next()方法
  2. 更好的语义
  3. 更广的适用性,不需要co模块配合,await命令后面,可以是 Promise 对象和原始类型的值
  4. 返回 Promise

4.5 Web workers

浏览器的渲染进程是多线程的,它包括如下线程:

  1. GUI渲染线程
  2. JS引擎线程,GUI渲染线程与JS引擎线程是互斥的
  3. 事件触发线程
  4. 定时触发器线程
  5. 异步http请求线程

由于,GUI渲染线程与JS引擎线程是互斥的,Web Worker 可以将一些耗时的数据处理操作从主线程中剥离,使主线程更加专注于页面渲染和交互。

Web Workers应用场景
  • 懒加载
  • 文本分析
  • 流媒体数据处理
  • canvas 图形绘制
  • 图像处理
  • ...
Web Workers的分类
  1. Dedicated Workers 专用线程

    专用线程仅能被创建它的脚本所使用(一个专用线程对应一个主线程)

  2. Shared Workers 共享线程

    共享线程能够在不同的脚本中使用(一个共享线程对应多个主线程)

  3. Service Workers 服务线程

专用线程示例:

// main.js
var worker = new Worker('task.js');
​
worker.onmessage = e => {
    console.log('worker.onmessage')
    this.regionalizationData = e.data
    this.doSmthing()
}
​
// task.js
const data = [1,2,3]
postMessage(data)

本文参考《代码整洁之道》(Robert C. Martin著,韩磊译)。

浙江大华技术股份有限公司-软研-智慧城市产品研发部招聘高级前端,欢迎来撩,有意向可发送简历到chen_zhen@dahuatech.com,长期有效

上一篇:十二、迭进

下一篇:十四、逐步改进