Proxy实现Schdular任务调度

154 阅读2分钟

目标:(受到vue3的启发)使用Proxy实现一个任务调度Schedular类

应用场景

将多个异步任务一次性添加进schedular中,执行器每次执行的最大任务数为max,其他的任务等待执行器中的任务执行完后自动补进来。

受到vue3 reactivity的Proxy拦截的启发,我们使用Proxy监听正在执行任务数来控制任务运行。

分析schedular

  1. Schedular有如下几个个属性:
  • max: 最大运行数;
  • count: 指正在运行数;
  • queue: 等待执行的任务队列。
  1. 这里我们需要监听(拦截)的就是count, 所以还需要一个proxy = new Proxy 来count。

执行过程

有5个异步任务task1,task2,......task5

将这五个任务添加进任务列表addTask

添加进去后就会立刻执行, 比如最多运行数n = 2; 正在运行数 count

那么task1,task2会立刻执行,其他的会被存起来

task执行前count++; 执行结束后count--;

等到task1或者task2某个先执行完,那么count--,此时触发Proxy的set, 从pendings中取出第一个候补任务,继续执行,count++; 执行完后count--。以此类推

schedular

那么Schedular类就可以写出来了

class Schedular{
    constructor(max){
        this.max = max;
        this.count = 0;
        this.pendings = [];
        // 拦截this,
        this.proxy = new Proxy(this,{
            async set(target,prop,newValue,receiver){
                // 如果不是count就放过
                if(prop !== 'count') return Reflect.set(target,prop,newValue,receiver)
                // 监听count, 如果count++了不做操作
                // 如果count--了,并且等待执行的列表中有任务就执行下面
                if(newValue < target.count && target.pendings.length > 0){
                    // 取出等待队列中的第一个
                    const candicate = target.pendings.shift();
                    // 执行
                    await target.runTask(candicate)
                    return Reflect.set(target, prop, newValue,receiver)
                }else{
                    return Reflect.set(target,prop,newValue,receiver)
                }
                
            }
        })
    }
    async addTask(task){
        if(this.count < this.max){
            this.runTask(task)
        }else{
            this.pendings.push(task)
        }

    }
    async runTask(task) {
        // 注意: 这里是proxy.count
        this.proxy.count++;
        console.log(`${task.id} is running`)
        await task();
        console.log(`${task.id} finished at ${Date.now() - global.startTime}`)
        // 注意: 这里是proxy.count
        this.proxy.count--;
    }
}

这里this.proxy用来监听this.count的。this.count--时,说明有一个任务执行完了,那么就从pendings中取出第一个候补任务运行。运行前count++, 运行结束count--;

Proxy的handler中set是支持async await的. 因为他作为拦截器本身就是函数

本来想使用Object.defineProperty,但是它的set不支持async await.

下面来测试一下吧; 创建五个异步任务


const taskCreator = (data, time) => function () {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data);
        }, time)
    })
}

// create 5 tasks
const task1 = taskCreator(1, 1000);
task1.id = 'task1';
const task2 = taskCreator(2, 2000);
task2.id = 'task2';
const task3 = taskCreator(3, 2000);
task3.id = 'task3';
const task4 = taskCreator(4, 2000);
task4.id = 'task4';
const task5 = taskCreator(5, 2000);
task5.id = 'task5';
// let tasks = [task1, task2, task3, task4, task5];
let tasks = [task1, task2, task3, task4, task5];
const schedular = new Schedular(2);

console.log('taskRunning');

global.startTime = Date.now()

tasks.forEach(task => {
    schedular.addTask(task)
});


我们期待的结果是: task1,task2先跑, task1结束, task3补进来运行,task2结束,task4补进来,task3结束,task5补进来,最后task4完成,task5结束. 如下图:

schedular1111111111111111111.png

控制台打印结果

taskRunning
task1 is running
task2 is running
task1 finished at 1008
task3 is running
task2 finished at 2007
task4 is running
task3 finished at 3013
task5 is running
task4 finished at 4017

task5 finished at 5046

完全符合预期

进一步优化

由于现在使用的queue是数组,shift时会塌陷,所以pendings我们使用链表结构, 并做好代码分割

LinkedList.js

const Node = require('./Node');
class LinkedList {
    constructor(name) {
        this.name = name;
        this.head = new Node('head');
        this.tail = new Node('tail');
        this.head.prev = null;
        this.head.next = this.tail;
        this.tail.prev = this.head;
        this.tail.next = null;
        this.count = 0;
    }
    enqueue(task) {
        let node = new Node(task);
        const currLast = this.tail.prev;
        currLast.next = node;
        node.prev = currLast;
        node.next = this.tail;
        this.tail.prev = node;
        this.count++;
    }
    dequeue() {
        const candicate = this.head.next;
        const next = candicate.next
        this.head.next = next;
        next.prev = this.head;
        this.count--;
        return candicate.value;
    }
    findFirst() {
        const next = this.head.next;
        if (next && next != this.tail) return next;
        return null;
    }
    findSecond() {
        const next = this.head.next;
        if (next && next != this.tail) {
            next = next.next;
            if (next && next != this.tail) {
                return next;
            } else {
                return null;
            }
        } else {
            return null
        }
    }
}
module.exports = LinkedList;

Node.js文件: 链表节点

// 
class Node {
    constructor(value) {
        this.value = value;
        this.prev = null;
        this.next = null;
    }
}
module.exports = Node;

Schedular.js

const LinkedList = require('./LinkedList');

class Schedular {
    constructor(max) {
        this.max = max;
        this.currentWorkingCount = 0;
        // this.currentWorkingCount = ref(0);
        this.pendings = new LinkedList();
        this.proxy = new Proxy(this, {
            async set(target, prop, newValue) {
                if (prop === 'currentWorkingCount') {
                    if (newValue < target.currentWorkingCount && target.pendings.count > 0) {
                        const candicate = target.pendings.dequeue();
                        await target.runTask(candicate)
                        return Reflect.set(target, prop, newValue)
                    }
                }
                return Reflect.set(target, prop, newValue)
            }
        })
    }
    async addTask(task) {
        if (this.currentWorkingCount < this.max) {
            await this.runTask(task)
        } else {
            this.cacheTask(task)
        }
    }
    async runTask(task) {
        this.proxy.currentWorkingCount++;
        console.log(`${task.id} is running`)
        await task();
        console.log(`${task.id} finished at ${Date.now() - global.startTime}`)
        this.proxy.currentWorkingCount--;
    }
    cacheTask(task) {
        this.pendings.enqueue(task);
    }
}

module.exports = Schedular;

uitls.js

const taskCreator = (data, time) => function () {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data);
            console.log(data);
        }, time)
    })
}

module.exports = {
    taskCreator
}

测试文件 /example.js

const { taskCreator } = require("./utils");
const Schedular = require("./Schedular");

// create 5 tasks
const task1 = taskCreator(1, 1000);
task1.id = 'task1';
const task2 = taskCreator(2, 2000);
task2.id = 'task2';
const task3 = taskCreator(3, 2000);
task3.id = 'task3';
const task4 = taskCreator(4, 2000);
task4.id = 'task4';
const task5 = taskCreator(5, 2000);
task5.id = 'task5';
// let tasks = [task1, task2, task3, task4, task5];
let tasks = [task1, task2, task3, task4, task5];
const schedular = new Schedular(2);

debugger
console.log('taskRunning');

global.startTime = Date.now()

tasks.forEach(task => {
    schedular.addTask(task)
});