客户端JavaScript的线程池设计详解

目录
  • 1.介绍:
  • 2.准备工作:
  • 3.测试spark-md5是否正常工作:
  • 4.线程池设计
  • 5.spark-md5对文件进行md5编码
  • 6.大量文件进行MD5加密并使用线程池优化
  • 总结

1.介绍:

本打算在客户端JavaScript进行机器学习算法计算时应用线程池来优化,就像()演示的神经网络。但是由于各种原因不了了之了。本次遇到了一个新的问题,客户端的MD5运算也是耗时操作,如果同时对多个字符串或文件进行MD5加密就可以使用线程池来优化。

2.准备工作:

到npm官网搜索spark-md5,到其github仓库下载spark-md5.js。该js文件支持AMD,CommonJS和web工作线程的模块系统,我们在实现线程池时,线程工作代码交给web工作线程处理。

3.测试spark-md5是否正常工作:

创建一个网页,再创建一个worker.js用于保存工作线程的代码。以下述代码测试,如果成功输出MD5编码,那么准备工作完成。

客户端网页代码

<script>
    let worker = new Worker("worker.js")
    worker.postMessage("Danny")
    worker.onmessage = function({data}) {
        console.log(data)
        worker.terminate()
    }
</script>

工作线程代码

self.importScripts("spark-md5.js")

self.onmessage = function({data}) {
    self.postMessage(self.SparkMD5.hash(data))
}

4.线程池设计

1. 目标:本次线程池设计的目标是初始创建n个初始线程,能够满足任意个线程请求,超出n的请求并不丢弃,而是等待到出现空闲线程后再分配之。

2. 基本设计思路:为了基本满足上述目标,至少要有一个线程分配功能,一个线程回收功能。

3. 线程分配功能设计:

  • 线程池满指的是线程池已经没有可用空闲线程
  • 通知对象是一个不可逆状态机,可以用Promise对象来实现
  • 阻塞请求队列存储Promise对象的resolve方法即可
  • 存储线程池中的线程使用数组即可,数组每个元素是一个对象,包括线程和线程状态
  • 返回给用户的可用线程还需要有线程在数组中的下标,在线程释放中会用到

4. 线程释放功能设计:

  • 线程释放功能需要接收一个参数,为线程的标识,3中设计该标识为数组下标
  • 当线程释放后,查看阻塞请求队列是否为空,如果不为空,说明有被阻塞的线程请求,此时令队首元素出队即可,执行resolve()通知对象的状态变更为Fulfilled

5. 实现线程池:

class MD5Pool {
    // worker用于存储线程
    worker = []
    // status是线程池状态
    status = "Idle"
    // 阻塞请求队列
    blockRequestQueue = []
    // size为用户希望的线程池的容量
    constructor(size) {
        for(let i = 0; i < size; i ++)
            this.worker.push({
                worker: new Worker("worker.js"),
                status: "Idle"
            })
    }

    // 线程池状态更新函数
    statusUpdate() {
        let sum = 0
        this.worker.forEach(({ status }) => {
            if(status === "Busy")
                sum ++
        })
        if(sum === this.worker.length)
            this.status = "Busy"
         else
            this.status = "Idle"
    }

    // 线程请求方法
    assign() {
        if(this.status !== "Busy") {
            // 此时线程池不满,遍历线程,寻找一个空闲线程
            for (let i = 0; i < this.worker.length; i++)
                if (this.worker[i].status === "Idle") {
                    // 该线程空闲,更新状态为忙碌
                    this.worker[i].status = "Busy"
                    // 更新线程池状态,如果这是最后一个空闲线程,那么线程池状态变为满
                    this.statusUpdate()
                    // 返回给用户该线程,和该线程的标识,标识用数组下标表示
                    return {
                        worker: this.worker[i].worker,
                        index: i
                    }
                }
        }
        else {
            // 此时线程池满
            let resolve = null
            // 创建一个通知对象
            let promise = new Promise(res => {
                // 取得通知对象的状态改变方法
                resolve = res
            })
            // 通知对象的状态改变方法加入阻塞请求队列
            this.blockRequestQueue.push(resolve)
            // 返回给请求者线程池已满信息和通知对象
            return {
                info: "full",
                wait: promise
            }
        }
    }

    // 线程释放方法,接收一个参数为线程标识
    release(index) {
        this.worker[index].status = "Idle"
        // 阻塞请求队列中的第一个请求出队,队列中存储的是promise的resolve方法,此时执行,通知请求者已经有可用的线程了
        if(this.blockRequestQueue.length)
            // 阻塞请求队列队首出列,并执行通知对象的状态改变方法
            this.blockRequestQueue.shift()()
        // 更新线程池状态,此时一定空闲
        this.status = "Idle"
    }
}

5.spark-md5对文件进行md5编码

说明:

在3的测试中spark-md5只是对简单字符串进行MD5编码,并非需要大量运算的耗时操作。spark-md5可以对文件进行MD5编码,耗时较多,实现如下。

注意:

spark-md5对文件编码时必须要对文件进行切片后再加密整合,否则不同文件可能会有相同编码。详情见github或npm。

// 在工作线程中引入spark-md5
self.importScripts("spark-md5.js")

let fd = new FileReader()
let spark = new self.SparkMD5.ArrayBuffer()

// 接收主线程发来的消息,是一个文件
self.onmessage = function(event) {
    // 获取文件
    let chunk = event.data
    // spark-md5要求计算文件的MD5必须切片计算
    let chunks = fileSlice(chunk)
    // 计算MD5编码
    load(chunks)
}

// 切片函数
function fileSlice(file) {
    let pos = 0
    let chunks = []
    // 将文件平均切成10分计算MD5
    const SLICE_SIZE = Math.ceil(file.size / 10)
    while(pos < file.size) {
        // slice可以自动处理第二个参数越界
        chunks.push(file.slice(pos, pos + SLICE_SIZE))
        pos += SLICE_SIZE
    }
    return chunks
}

// MD5计算函数
async function load(chunks) {
    for(let i = 0; i < chunks.length; i ++) {
        fd.readAsArrayBuffer(chunks[i])
        // 在这里希望节约空间,因此复用了FileReader,而不是每次循环新创建一个FileReader。需要等到FileReader完成read后才可以进行下一轮复用,因此用await阻塞。
        await new Promise(res => {
            fd.onload = function(event) {
                spark.append(event.target.result)
                if(i === chunks.length - 1) {
                    self.postMessage(spark.end())
                }
                res()
            }
        })
    }
}

6.大量文件进行MD5加密并使用线程池优化

下面的测试代码就是对上文所述的拼接

网页代码

<input id="input" type="file" multiple onchange="handleChanged()"/>
<body>
    <script>
        class MD5Pool {
            worker = []
            status = "Idle"
            blockRequestQueue = []
            constructor(size) {
                for(let i = 0; i < size; i ++)
                    this.worker.push({
                        worker: new Worker("worker.js"),
                        status: "Idle"
                    })
            }

            statusUpdate() {
                let sum = 0
                this.worker.forEach(({ status }) => {
                    if(status === "Busy")
                        sum ++
                })
                if(sum === this.worker.length)
                    this.status = "Busy"
                 else
                    this.status = "Idle"
            }

            assign() {
                if(this.status !== "Busy") {
                    for (let i = 0; i < this.worker.length; i++)
                        if (this.worker[i].status === "Idle") {
                            this.worker[i].status = "Busy"
                            this.statusUpdate()
                            return {
                                worker: this.worker[i].worker,
                                index: i
                            }
                        }
                }
                else {
                    let resolve = null
                    let promise = new Promise(res => {
                        resolve = res
                    })
                    this.blockRequestQueue.push(resolve)
                    return {
                        info: "full",
                        wait: promise
                    }
                }
            }

            release(index) {
                this.worker[index].status = "Idle"
                // 阻塞请求队列中的第一个请求出队,队列中存储的是promise的resolve方法,此时执行,通知请求者已经有可用的线程了
                if(this.blockRequestQueue.length)
                    this.blockRequestQueue.shift()()
                this.status = "Idle"
            }
        }

        // input点击事件处理函数
        function handleChanged() {
            let files = event.target.files
            // 创建一个大小为2的MD5计算线程池
            let pool = new MD5Pool(2)
            // 计算切片文件的MD5编码
            Array.prototype.forEach.call(files, file => {
                getMD5(file, pool)
            })
        }

        // 获取文件的MD5编码的函数,第一个参数是文件,第二个参数是MD5线程池
        async function getMD5(chunk, pool) {
            let thread = pool.assign()
            // 如果info为full,那么说明线程池线程已被全部占用,需要等待
            if(thread.info === "full") {
                // 获取线程通知对象
                let wait = thread.wait
                // 等到wait兑现时说明已经有可用的线程了
                await wait
                thread = pool.assign()
                let { worker, index } = thread
                worker.postMessage(chunk)
                worker.onmessage = function (event) {
                    console.log(event.data)
                    pool.release(index)
                }
            } else {
                let { worker, index } = thread
                worker.postMessage(chunk)
                worker.onmessage = function (event) {
                    console.log(event.data)
                    pool.release(index)
                }
            }
        }
    </script>
</body>

工作线程代码

self.importScripts("spark-md5.js")

let fd = new FileReader()
let spark = new self.SparkMD5.ArrayBuffer()

self.onmessage = function(event) {
    // 获取文件
    let chunk = event.data
    // spark-md5要求计算文件的MD5必须切片计算
    let chunks = fileSlice(chunk)
    // 计算MD5编码
    load(chunks)
}

// 切片函数
function fileSlice(file) {
    let pos = 0
    let chunks = []
    // 将文件平均切成10分计算MD5
    const SLICE_SIZE = Math.ceil(file.size / 10)
    while(pos < file.size) {
        // slice可以自动处理第二个参数越界
        chunks.push(file.slice(pos, pos + SLICE_SIZE))
        pos += SLICE_SIZE
    }
    return chunks
}

// MD5计算函数
async function load(chunks) {
    for(let i = 0; i < chunks.length; i ++) {
        fd.readAsArrayBuffer(chunks[i])
        // 在这里希望节约空间,因此复用了FileReader,而不是每次循环新创建一个FileReader。需要等到FileReader完成read后才可以进行下一轮复用,因此用await阻塞。
        await new Promise(res => {
            fd.onload = function(event) {
                spark.append(event.target.result)
                if(i === chunks.length - 1) {
                    self.postMessage(spark.end())
                }
                res()
            }
        })
    }
}

随机选取18个文件进行MD5编码,结果如下

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • 深入理解Java线程池从设计思想到源码解读

    线程池:从设计思想到源码解析 前言初识线程池线程池优势线程池设计思路 深入线程池构造方法任务队列拒绝策略线程池状态初始化&容量调整&关闭 使用线程池ThreadPoolExecutorExecutors封装线程池 解读线程池execute()addWorker()Worker类runWorker()processWorkerExit() 前言 各位小伙伴儿,春节已经结束了,在此献上一篇肝了一个春节假期的迟来的拜年之作,希望读者朋友们都能有收获. 根据穆氏哲学,投入越多,收获越大.我作此文时

  • Node.js事件循环(Event Loop)和线程池详解

    Node的"事件循环"(Event Loop)是它能够处理大并发.高吞吐量的核心.这是最神奇的地方,据此Node.js基本上可以理解成"单线程",同时还允许在后台处理任意的操作.这篇文章将阐明事件循环是如何工作的,你也可以感受到它的神奇. 事件驱动编程 理解事件循环,首先要理解事件驱动编程(Event Driven Programming).它出现在1960年.如今,事件驱动编程在UI编程中大量使用.JavaScript的一个主要用途是与DOM交互,所以使用基于事件

  • 深入了解Java线程池:从设计思想到源码解读

    目录 为什么需要线程池 线程池设计思路 线程池的工作机制 线程池的参数及使用 线程池的状态 提交任务 任务队列 线程工厂 拒绝策略 关闭线程池 Executors 静态工厂 合理地配置线程池 线程池的监控 源码分析 execute addWorker Worker runWorker getTask processWorkerExit 面试题 为什么需要线程池 我们知道创建线程的常用方式就是 new Thread() ,而每一次 new Thread() 都会重新创建一个线程,而线程的创建和销毁

  • java 可重启线程及线程池类的设计(详解)

    了解JAVA多线程编程的人都知道,要产生一个线程有两种方法,一是类直接继承Thread类并实现其run()方法:二是类实现Runnable接口并实现其run()方法,然后新建一个以该类为构造方法参数的Thread,类似于如下形式: Thread t=new Thread(myRunnable).而最终使线程启动都是执行Thread类的start()方法. 在JAVA中,一个线程一旦运行完毕,即执行完其run()方法,就不可以重新启动了.此时这个线程对象也便成了无用对象,等待垃圾回收器的回收.下次

  • 客户端JavaScript的线程池设计详解

    目录 1.介绍: 2.准备工作: 3.测试spark-md5是否正常工作: 4.线程池设计 5.spark-md5对文件进行md5编码 6.大量文件进行MD5加密并使用线程池优化 总结 1.介绍: 本打算在客户端JavaScript进行机器学习算法计算时应用线程池来优化,就像()演示的神经网络.但是由于各种原因不了了之了.本次遇到了一个新的问题,客户端的MD5运算也是耗时操作,如果同时对多个字符串或文件进行MD5加密就可以使用线程池来优化. 2.准备工作: 到npm官网搜索spark-md5,到

  • Java ExecutorService四种线程池使用详解

    1.引言 合理利用线程池能够带来三个好处.第一:降低资源消耗.通过重复利用已创建的线程降低线程创建和销毁造成的消耗.第二:提高响应速度.当任务到达时,任务可以不需要的等到线程创建就能立即执行.第三:提高线程的可管理性.线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控.但是要做到合理的利用线程池,必须对其原理了如指掌. 2.线程池使用 Executors提供的四种线程 1.newCachedThreadPool创建一个可缓存线程池

  • java简单实现多线程及线程池实例详解

    本文为大家分享了java多线程的简单实现及线程池实例,供大家参考,具体内容如下 一.多线程的两种实现方式 1.继承Thread类的多线程 /** * 继承Thread类的多线程简单实现 */ public class extThread extends Thread { public void run(){ for(int i=0;i<100;i++){ System.out.println(getName()+"-"+i); } } public static void mai

  • Spring boot注解@Async线程池实例详解

    这篇文章主要介绍了Spring boot注解@Async线程池实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 从Spring3开始提供了@Async注解,该注解可以被标注在方法上,以便异步地调用该方法.调用者将在调用时立即返回,方法的实际执行将提交给Spring TaskExecutor的任务中,由指定的线程池中的线程执行. 1. TaskExecutor Spring异步线程池的接口类,其实质是java.util.concurrent

  • Python定时器线程池原理详解

    这篇文章主要介绍了Python定时器线程池原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 定时器执行循环任务: 知识储备 Timer(interval, function, args=None, kwargs=None) interval ===> 时间间隔 单位为s function ===> 定制执行的函数 使用threading的 Timer 类 start() 为通用的开始执行方法 cancel ()为取消执行的方法 普通单次

  • java中常见的6种线程池示例详解

    之前我们介绍了线程池的四种拒绝策略,了解了线程池参数的含义,那么今天我们来聊聊Java 中常见的几种线程池,以及在jdk7 加入的 ForkJoin 新型线程池 首先我们列出Java 中的六种线程池如下 线程池名称 描述 FixedThreadPool 核心线程数与最大线程数相同 SingleThreadExecutor 一个线程的线程池 CachedThreadPool 核心线程为0,最大线程数为Integer. MAX_VALUE ScheduledThreadPool 指定核心线程数的定时

  • python爬虫线程池案例详解(梨视频短视频爬取)

    python爬虫-梨视频短视频爬取(线程池) 示例代码 import requests from lxml import etree import random from multiprocessing.dummy import Pool # 多进程要传的方法,多进程pool.map()传的第二个参数是一个迭代器对象 # 而传的get_video方法也要有一个迭代器参数 def get_video(dic): headers = { 'User-Agent':'Mozilla/5.0 (Wind

  • Python异步爬虫多线程与线程池示例详解

    目录 背景 异步爬虫方式 多线程,多进程(不建议) 线程池,进程池(适当使用) 单线程+异步协程(推荐) 多线程 线程池 背景 当对多个url发送请求时,只有请求完第一个url才会接着请求第二个url(requests是一个阻塞的操作),存在等待的时间,这样效率是很低的.那我们能不能在发送请求等待的时候,为其单独开启进程或者线程,继续请求下一个url,执行并行请求 异步爬虫方式 多线程,多进程(不建议) 好处:可以为相关阻塞的操作单独开启线程或者进程,阻塞操作就可以异步会执行 弊端:不能无限制开

  • Java线程池 ThreadPoolExecutor 详解

    目录 一 为什么要使用线程池 二 线程池原理详解 2.1 线程池核心组成 2.2 Execute 原理 三 线程池的使用 3.1 创建线程池 3.1.1 自定义线程池 3.1.2 功能线程池 3.1.3 功能线程池存在的问题 3.2 向线程池提交任务 3.3 关闭线程池 3.4 自定义线程池需要考虑因素 一 为什么要使用线程池 对于操作系统而言,创建一个线程的代价是十分昂贵的, 需要给它分配内存.列入调度,同时在线程切换时要执行内存换页,清空 CPU 缓存,切换回来时还要重新从内存中读取信息,破

  • jdk自带线程池实例详解

    二.简介 多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力,但频繁的创建线程的开销是很大的,那么如何来减少这部分的开销了,那么就要考虑使用线程池了.线程池就是一个线程的容器,每次只执行额定数量的线程,线程池就是用来管理这些额定数量的线程. 三.涉及线程池的类结构图 其中供我们使用的,主要是ThreadPoolExecutor类. 四.如何创建线程池 我们创建线程池一般有以下几种方法: 1.使用Executors工厂类 Executor

随机推荐