客户端JavaScript的线程池设计

1.介绍:

本打算在客户端JavaScript进行机器学习算法计算时应用线程池来优化,就像(http://playground.tensorflow.org)演示的神经网络。但是由于各种原因不了了之了。本次遇到了一个新的问题,客户端的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编码,结果如下
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,以下是一个简单的Java HTTP服务器程序和HTTP客户端程序的示例,采用线程池编程技术处理客户端请求,并支持多客户端同时访问。 HTTP服务器程序: ```java import java.io.*; import java.net.*; import java.util.concurrent.*; public class HttpServer { private static final int PORT = 8080; private static final int THREAD_POOL_SIZE = 10; public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(PORT); ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); while (true) { Socket socket = serverSocket.accept(); executorService.execute(new HttpRequestHandler(socket)); } } } class HttpRequestHandler implements Runnable { private static final String WEB_ROOT = "www"; private static final String DEFAULT_PAGE = "index.html"; private Socket socket; public HttpRequestHandler(Socket socket) { this.socket = socket; } @Override public void run() { try { BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); String line = reader.readLine(); if (line == null) { socket.close(); return; } String[] tokens = line.split(" "); String method = tokens[0]; String url = tokens[1]; String httpVersion = tokens[2]; if (!method.equals("GET")) { sendError(405, "Method Not Allowed"); return; } if (url.equals("/")) { url += DEFAULT_PAGE; } File file = new File(WEB_ROOT, url.substring(1)); if (!file.exists()) { sendError(404, "File Not Found"); return; } String contentType = guessContentType(file.getName()); int contentLength = (int) file.length(); byte[] content = new byte[contentLength]; FileInputStream fileInputStream = new FileInputStream(file); fileInputStream.read(content); fileInputStream.close(); sendResponse(200, "OK", contentType, content); } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } private void sendResponse(int statusCode, String statusText, String contentType, byte[] content) throws IOException { OutputStream outputStream = socket.getOutputStream(); PrintWriter writer = new PrintWriter(outputStream); writer.println("HTTP/1.1 " + statusCode + " " + statusText); writer.println("Content-Type: " + contentType); writer.println("Content-Length: " + content.length); writer.println(); writer.flush(); outputStream.write(content); outputStream.flush(); } private void sendError(int statusCode, String statusText) throws IOException { String message = "<html><head><title>" + statusText + "</title></head><body><h1>" + statusText + "</h1></body></html>"; byte[] content = message.getBytes(); sendResponse(statusCode, statusText, "text/html", content); } private String guessContentType(String fileName) { if (fileName.endsWith(".html") || fileName.endsWith(".htm")) { return "text/html"; } else if (fileName.endsWith(".css")) { return "text/css"; } else if (fileName.endsWith(".js")) { return "text/javascript"; } else if (fileName.endsWith(".png")) { return "image/png"; } else if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) { return "image/jpeg"; } else { return "application/octet-stream"; } } } ``` HTTP客户端程序: ```java import java.io.*; import java.net.*; public class HttpClient { private static final String HOST = "localhost"; private static final int PORT = 8080; public static void main(String[] args) throws IOException { Socket socket = new Socket(HOST, PORT); OutputStream outputStream = socket.getOutputStream(); PrintWriter writer = new PrintWriter(outputStream); writer.println("GET / HTTP/1.1"); writer.println("Host: " + HOST); writer.println(); writer.flush(); InputStream inputStream = socket.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String line; while ((line = reader.readLine()) != null) { System.out.println(line); } socket.close(); } } ``` 这个示例程序中,服务器程序会监听8080端口,并使用线程池处理客户端请求。当客户端发出GET请求时,服务器会返回相应的静态文件。客户端程序会连接到服务器的8080端口,并发送GET请求。服务器返回的响应会被输出到控制台。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vanghua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值