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.总结
特别注意,只有通过个人主页博客或者个人介绍中方式,才能获取源码