Autojs基础-多线程(threads)

0 阅读11分钟

1.前言

我前面就提到autojs中使用ui后,无法再使用阻塞函数。使用ui后,ui会占用整个脚本的主线程,我们为了能够使用sleep等阻塞函数,可以再开个线程用于跑逻辑代码。脚本启动时,会先加载ui页面,然后我们设置个悬浮窗,包括配置、启动等功能,等配置基本信息后,点击启动会开启一个线程跑逻辑代码。然后点击停止,就会将逻辑线程关掉,实现了脚本的配置、启动和暂停等功能。

一般逻辑线程中,我还会再开启一个线程来完成主要任务,比如游戏脚本的自动任务功能。为什么会这样设计?这样设计是为了监控一些游戏掉线等特殊情况出现,如果出现这种情况,可以方便关闭任务线程,然后再开启一个新线程,不至于出现特殊情况就会导致脚本卡死。

我们可以将线程理解成河流,主线程理解为主河流,主河流是不允许阻塞的否则就会导致溢出,而在主线程上开的分线程就可以理解成分流,我们可以引出多条分流来保证同时流淌,分流阻塞对主流也不会有影响。如果有分流出现问题,我们可以将这个分流关闭,然后再引出新的分流,只要保证主流不阻塞,程序就能一直跑下去。

每个成熟的脚本都会有线程的内容,其实功能都类似。我认为线程这部分内容是非常重要的,脚本设计好线程关系能够更好保证脚本的兼容性,保证脚本在特殊环境下也能正常运行。同时,对于需要大量时间处理的内容,我们也可以分出一个线程来处理,提高脚本的运行速度。

2.函数

1.概况

线程部分一般需要配合多个函数使用,我就不按照官方文档的顺序介绍线程了。说实话,官方文档对线程方面的内容是非常多的,质量也很高,大家可以认真看下官方对于这部分的介绍。我后面会根据自己理解简单介绍下线程的使用,线程这部分在脚本开发时可以玩出花活来,很多经验都是在真正脚本开发时才能学到。

这部分还有关于定时器函数的介绍,我们一般使用js的定时器就好了,两者功能类似。我们完全没必要花时间来学习定时器的内容,如果感兴趣可以自行学习。

2.start、isAlive与interrupt

start函数用于启动一个线程,需要传递线程操作函数一个参数,参数类型为函数,返回启动后的线程,返回类型为线程。这个启动的线程要么运行完成后自动停止,要么通过中断的方式停止。因为线程之间是异步运行的,我们使用这个函数时,需要接收返回的线程,通过返回的线程判断线程运行情况。

isAlive函数用于判断线程是否存活,返回线程存活情况,返回类型为boolean。

interrupt函数用于中断线程运行,中断后立即判断线程是运行,获得到的信息是还在运行,但是写了代码想判断就会获取到线程已经停止。这个时间可以忽略不计,但是直接获取又得到还在运行,又无法获取到中断时间,好在不影响最终的功能。

let i = 0;

// 启动一个线程
let currentThread = threads.start(function () {
    // 每500毫秒加1
    for (let index = 0; index < 100; index++) {
        i++;
        sleep(500);
    }
});

// 判断线程是否存活
let hasAlive = currentThread.isAlive();
console.log("线程是否存活:" + hasAlive);


// 当i小于100时,一直循环
while (i < 100) {
    // 当i等于10时,中断线程运行,强制结束循环
    if (i >= 10) {
        // 中断线程运行
        currentThread.interrupt();
        // 强制结束循环
        break;
    }
}

// 等待线程关闭
let startTime = new Date().getTime();
// 中断后,判断线程是否存活
hasAlive = currentThread.isAlive();
console.log("中断后,线程是否存活:" + hasAlive);


// 直至线程关闭
while (currentThread.isAlive()) {
    
}
let endTime = new Date().getTime();
console.log("线程已经关闭,耗时" + (endTime - startTime) + "毫秒");


// 延迟2秒,看i是否增加,这个值能够看判断出线程是否停止
sleep(2000);
console.log("最终值:" + i);

这个中断线程的时间大部分是0毫秒,有个别时候1毫秒,大家清楚就行了,不会影响最后的功能。

3.currentThread

获取当前线程,返回线程信息,返回类型为线程。如果不手动开启线程,脚本会运行在主线程里面,我们可以将主线程关闭,就会发现后面的代码无法执行了。虽然线程中断的时间可以忽略不计,但是我后面的代码太简单了,很可能没终止就会输出,这样严谨代码,最好加上等待线程中断的判断。

// 获取当前线程
let currentThread = threads.currentThread();

// 打印线程信息
console.log(currentThread);

// 中断线程
currentThread.interrupt();

// 等待线程中断成功
while (currentThread.isAlive()) {
    console.log("等待线程中断...");
}

console.log("还能输出");

4.disposable

新建一个Disposable对象,用于获取线程的数据,主要用于线程通信,返回Disposable对象,返回类型Disposable。

Disposable对象有setAndNotify和blockedGet两个函数。setAndNotify函数用于将数据传递给主线程,并且通知主线程获取数据,需要传递数据一个参数,参数能够接受数字、字符串、对象等常用参数。blockedGet函数用于setAndNotify函数获取的数据,返回类型对应setAndNotify函数传入类型,但是传入数字,返回后会变成字符串,需要特别注意。blockedGet函数是一个阻塞函数,不接收到数据,下面的代码无法运行。说到这里,除了基础用法外,在多线程中可以让脚本运行到了不需要另一个线程的数据的位置,在需要另一个线程数据的前面加上blockedGet函数,然后在需要获取数据的线程上面随便传递个数据过去,告诉计算已经完成。下面的代码,就是比较两种方式获取的数据是一致的,我已经用心给大家演示功能了,请小伙伴们详细看下代码。


let currentDisposable = threads.disposable();
let i = 0;

// 启动一个线程
let currentThread = threads.start(function () {
    // 每500毫秒加1
    for (let index = 0; index < 10; index++) {
        i++;
        sleep(500);
    }

    // 通知主线程接收数据
    currentDisposable.setAndNotify(i);
});
console.log("运行了");
// 获取线程中的数据
let newI = currentDisposable.blockedGet();
console.log(newI);
console.log(i);
console.log("数据+1:" + (newI + 1));

// 将数据转换为数字
let newNumberI = Number(newI);
console.log("转换后数据+1:" + (newNumberI + 1));

// 线程运行结束,会自动关闭
console.log("线程是否存活:" + currentThread.isAlive());

5.atomic与lock

用到线程就会考虑到线程安全问题,说实话在脚本开发中,真的很少用到,但是这是线程的重要内容,小伙伴们可以当做一个知识扩充,很多编程语言也会有线程的内容,线程安全其实都差不多。这两个函数都是关于线程安全的函数,atomic函数用于自动创建一个数字对象,然后通过调用这个对象的函数来实现增减和获取数据是安全的;lock函数就是线程锁,一般线程都会有这个函数,在变量操作开启锁,数据操作完成后关闭锁,这样就能保证同一个时间多个线程只有一个线程能操作变量。

atomic有非常多的函数,请根据表格的介绍进行使用。

基础操作get()获取原子变量的当前值。
set(long newValue)设置原子变量的值为新值。
算术运算 (返回新值)incrementAndGet()先自增1,然后返回自增后的新值。
addAndGet(long delta)先加上指定值delta,然后返回相加后的新值。
decrementAndGet()先自减1,然后返回自减后的新值。
算术运算 (返回旧值)getAndIncrement()先返回自增前的旧值,然后自增1。
getAndAdd(long delta)先返回相加前的旧值,然后加上delta
getAndDecrement()先返回自减前的旧值,然后自减1。
比较与交换(CAS)compareAndSet(long expect, long update)如果当前值等于期望值expect,则原子地设置为新值update。返回操作是否成功的布尔值。这是实现无锁算法的基石。
值交换getAndSet(long newValue)设置为新值,并返回设置之前的旧值。
函数式更新updateAndGet(LongUnaryOperator op)更新当前值,并返回更新后的新值。
getAndUpdate(LongUnaryOperator op)更新当前值,并返回更新前的旧值。

lock只有lock和unlock两个函数,分别代表上锁和解锁,上锁时候只允许当前线程运行,解锁时候允许多个线程运行。

没有使用线程安全两个线程同时计算,计算时加上延迟才能体现竞争关系,如果不加延迟又这么少的数量,没有意义。加上延迟后,没有使用线程安全的情况下,结果为48。

let i = 0;


// 启动一个线程
let currentThread = threads.start(function () {
    for (let index = 0; index < 25; index++) {
        // 加1后获取新值
        console.log("分线程:" + (++i));
        sleep(200);
    }

});

for (let index = 0; index < 25; index++) {
    // 加1后获取新值
    console.log("主线程:" + (++i));
    sleep(200);
}

根据atomic函数方式实现线程安全,线程安全是保证最后计算数量符合逻辑,不出现重复计算,但是但是数字输出先后是不保证的。

let currentAtomic = threads.atomic();

// 设置初始值为10
currentAtomic.set(0);
// 启动一个线程
let currentThread = threads.start(function () {
    for (let index = 0; index < 25; index++) {
        // 加1后获取新值
        console.log("分线程:"+ currentAtomic.incrementAndGet());
        sleep(200);
    }

});

for (let index = 0; index < 25; index++) {
    // 加1后获取新值
    console.log("主线程:" + currentAtomic.incrementAndGet());
    sleep(200);
}

根据lock锁的方式实现线程安全,功能和automic类似,更加灵活简单。

let currentLock = threads.lock();
let i = 0;

// 启动一个线程
let currentThread = threads.start(function () {
    for (let index = 0; index < 25; index++) {
        // 上锁
        currentLock.lock();
        // 加1后获取新值
        console.log("分线程:" + (++i));
        // 解锁
        currentLock.unlock();
        sleep(200);
    }

});

for (let index = 0; index < 25; index++) {
    // 上锁
    currentLock.lock();
    // 加1后获取新值
    console.log("主线程:" + (++i));
    // 解锁
    currentLock.unlock();
    sleep(200);
}

6.join

等待线程运行完成,再运行后面的脚本。

let i = 0;

// 启动一个线程
let currentThread = threads.start(function () {
    for (let index = 0; index < 25; index++) {
        // 加1后获取新值
        console.log("分线程:" + (++i));
        sleep(200);
    }

});

// 等待线程运行完成
currentThread.join();

for (let index = 0; index < 25; index++) {
    // 加1后获取新值
    console.log("主线程:" + (++i));
    sleep(200);
}

可以看出,当分线程运行结束后,主线程才会运行。

7.waitFor

线程执行需要一定时间,这个函数用于线程开始执行时,运行后面的脚本。这个函数的使用场景,我一时真无法想起,可以特殊情况下使用吧。线程启动起来应该花费不了多少时间,我感觉这个函数用处不大。

let i = 0;

// 启动一个线程
let currentThread = threads.start(function () {
    for (let index = 0; index < 25; index++) {
        // 加1后获取新值
        console.log("分线程:" + (++i));
        sleep(200);
    }

});

// 等待线程开始运行
currentThread.waitFor();

for (let index = 0; index < 25; index++) {
    // 加1后获取新值
    console.log("主线程:" + (++i));
    sleep(200);
}

8.sync

将函数变成同步函数,需要传递函数一个参数,参数类型为函数,返回同步函数,返回类型为函数。变成同步函数的作用是同一时间只有一个线程能调用这个函数,也能实现线程安全,加上上面介绍的automic以及lock函数,一共有三种方式可以实现线程安全。

我为了节省时间,只运行了50次,哪怕加上延迟,也会有概率不触发线程竞争问题。我现在就不保证线程安全的情况下,在每个线程中累加1000次。

let i = 0;
function add(x) {
    i += x;
}

// 启动一个线程
let currentThread = threads.start(function () {
    for (let index = 0; index < 1000; index++) {
        add(1);
    }

});

for (let index = 0; index < 1000; index++) {
    add(1);
}

// 等待线程运行完成
currentThread.join();
console.log(i);

正常应该得到2000的结果,然而最终得到了1656,差距还是很大的。

通过sync函数将累加函数变成同步函数后,能够得到正常的数据。

let i = 0;
function add(x) {
    i += x;
}

// 获取异步函数
let syncAdd = sync(add);

// 启动一个线程
let currentThread = threads.start(function () {
    for (let index = 0; index < 1000; index++) {
        syncAdd(1);
    }

});

for (let index = 0; index < 1000; index++) {
    syncAdd(1);
}

// 等待线程运行完成
currentThread.join();
console.log(i);

3.总结

特别注意,只有通过个人主页博客或者个人介绍中方式,才能获取源码