关于aardio多线程调用python防止界面卡顿

在 aardio 中使用多线程

问题

我们虽然现在界面也有了,逻辑也有了,但是我们现在会遇到一个问题,那就是长时间耗时的项目会导致我们的界面卡顿,我们来看一个例子

import time
def test(winform):
	time.sleep(1)
	winform.edit.text = "你好世界"

我们调用这一串 python 代码,就能发现我们的界面卡主了,迟迟出不来

这个代码是可以延伸的,我们这里的 time.sleep() 只是给他增加了一个耗时的任务,实际上这个耗时的任务可以是爬虫正在等待服务器响应,或者是CPU正在进行大规模计算,这都有可能会导致我们的线程卡死,出现界面的问题

修改一下代码

我们修改一下我们的代码让我们的界面卡死更加直观

image-20221005130154568

现在我们新增了一个按钮,然后给这个按钮绑定一个逻辑,上面那一串代码只有在点击了我们的按钮之后才会执行

winform.button.oncommand = function(id,event){
	py3.main.test(py3.export(winform))
}

image-20221005130257039

这个时候就会看到我们的窗口直接就未响应了,如果有其他的按钮或者操作,我们都无法继续进行操作

多线程

这个时候我们就需要引入多线程,将我们这些耗时的任务给丢给我们的子线程,不能让他卡死我们的主线程

python 里的多线程

在这之前我们需要先看看 python 的多线程是怎么实现的

import threading
import os
import time

def waste_time(x):
    thread_id = threading.currentThread()
    print(f"{thread_id.ident} 当前开始运行,即将等待 {x} 秒")
    time.sleep(x)
    print(f'\n{thread_id.ident} 线程运行结束')
    
def start():
    threads = [threading.Thread(target= waste_time, args=(3,)) for _ in range(6)]
    for i in threads:
        i.start()

    for i in threads:
        i.join()
    
if __name__ == '__main__':
    start()

我们这里创建了一个耗时的多线程,为了让我们每个线程之间都能彼此区分开来,我们这里使用了 threading.currentThread()方法,这个方法可以输出当前的线程号,这样我们就能知道都是那些线程运行结束和运行开始了

然后我们使用了他里面的一个属性 ident,可以让我们的显示更加清晰,当然你不加也是可以的

输出结果

20084 当前开始运行,即将等待 3 秒
20604 当前开始运行,即将等待 3 秒
8672 当前开始运行,即将等待 3 秒
20956 当前开始运行,即将等待 3 秒
1080 当前开始运行,即将等待 3 秒
20120 当前开始运行,即将等待 3 秒

20084 线程运行结束
20604 线程运行结束
20956 线程运行结束
8672 线程运行结束
20120 线程运行结束
1080 线程运行结束
尝试直接把这个放到 aardio 里面

我们试一试如果直接用 python 的多线程直接丢到我们的 aardio 里面,还会不会出现之前的那种界面卡死的情况,不过为了能够能够被放进去,我们需要修改一下我们的代码

首先还是要传入一个 mainForm 参数,但是我们这里是多线程,需要每个参数都传递一次,非常浪费性能,我们可以考虑使用全局变量把我们的 mainForm 定义在外面

import threading
import os
import time

mainForm = None

def waste_time(x):
    thread_id = threading.currentThread()
    print(f"{thread_id.ident} 当前开始运行,即将等待 {x} 秒")
    time.sleep(x)
    print(f'{thread_id.ident} 线程运行结束')
    # 将结果打印到我们的 编辑框上
    mainForm.edit.text += f"{thread_id.ident} 当前开始运行,即将等待 {x} 秒\r"
    mainForm.edit.text += f"{thread_id.ident} 线程运行结束\r"
    
def start():
    threads = [threading.Thread(target= waste_time, args=(3,)) for _ in range(6)]
    for i in threads:
        i.start()

    for i in threads:
        i.join()
    
# if __name__ == '__main__':
#     start()

然后我们的 aardio 代码也要做出相应的修改

py3.main.mainForm = py3.export(winform)

winform.button.oncommand = function(id,event){
	py3.main.start()
}

image-20221005132225196

然后就能看到虽然我们的控制台有输出,但是我们的程序依旧卡死了,这说明了 python 自带的多线程没有办法在 aardio 里面使用,如果你原先的程序里面包含多线程想要使用图形界面的同时还想使用图形界面,你可能就得考虑把多线程给拆分成单线程了,但是没有关系,我们还有其他方法可以实现多线程

GIL 锁

我们来看一下官方给我们的案例,他这里提到了一个新的概念叫 GIL 锁,那么这个 GIL 锁是什么意思呢?

我们只做一个概念性的理解,不做深度展开

存钱取钱问题

想象一个场景,现在你在取钱,你的账户里面只有 100 元,然后你现在想要提取 100 元,这个时候肯定是没有问题的,如果成功就会把钱给提取出来,如果失败就不会提取出钱而是给出一个提示

但是想象一下,假如有一个人想要做坏事,他让一个人在海南岛的ATM机上面取钱,然后自己在北京取钱,他们两个商量好了,一起按取钱,然后因为网络的延时,我们的ATM在判断的时候就有两个线程在和他抢夺 if 的判断,最终两个人都进入了 if 判断,然后 if 判断执行了两次,最终成功取出了 200 元,而这个账户却只有 100 元

这个时候就需要我们的锁登场了,锁的作用就是,在我们另一个程序访问一个内存地址或者在修改IO的时候不让其他人访问,就好像一个厕所把门锁住了,外面的人哪怕再急也要等里面的人上完,如果加了锁,我们下面的代码就不会出现问题,因为如果上一个 if 判断没有判断结束,我们的下一个就不会进入 if 判断,而是会进行等待

money = 100
extract = 100
result = money - extract
if money - extract >=0:
    money = result
    print(f"取钱成功,当前你还有 {result} 元")
else:
    print("当前余额不足")

我们现在知道了锁是什么,但是这个都是显式的加锁,是我们手动在加,但是实际上我们的 Python 在我们看不到的地方也加了一个全局锁,这个锁可以让我们在更多正常的情况下免去很多麻烦,但是虽然这个锁可以保证我们能够免去很多麻烦,但是同时因为有锁的存在,也限制了一些存在,比如大家都在诟病的 python 没有真正的多线程,都是伪多线程

我们想要实现真正的多线程就是要解除我们的 GIL 全局锁,不过不用害怕,这个解除锁定的方式非常简单,只需要记住一个 API 即可,那就是 py3.releaseThread();

aardio 给出的案例

我们先来看一看 aardio 给出的多线程解决方案

image-20221005133850304

/*
要点1、一定要在主线程导入python库
主线程会负责创建python虚拟机,主线程退出不能再在任何线程调用python.
*/
import py3;

/*
要点2、一定要在主线程释放GIL,之后任何python调用都在在py.lock里执行
注意,只有你确实要在多个线程并发调用python时,才需要使用GIL。
*/
py3.releaseThread();

pyThread = function(winform){
	import py3; 
	
	/*
	要点3、启用GIL以后,任何python调用都必须在py.lock里执行
	python并不支持真正的多线程,所有调用都要加锁互斥运行。
	*/
	py3.lock(
		function(){
			var hashlib = py3.import("hashlib"); 
			var md5 = hashlib.md5()
			md5.update( raw.buffer("注意这个函数的参数不是字符串而是字节数组(相当于aardio中的buffer)") ); 
			winform.edit.print(thread.getId(), tostring(md5.hexdigest()) ); 				
		}
	) 
}


winform.button.oncommand = function(id,event){
   
	for(i=1;10;1){
    	thread.invoke( pyThread,winform )  
    }
}

我们看到官方的案例这次有了一个界面,案例也比之前多了很多的中文说明,说明官方确实很想让大家弄懂,而且这一节的难度也确实比之前提升了很多,但是依旧是老问题

官方选择的案例依旧超出了许多新手的见识,因为官方在这里调用了一个 md5 方法

在 python 中有一个专门负责加密的库叫 hashlib, 这个库里面有许多主流的加密方法,我们这里不会将其他的方法,就只是给大家简单的介绍一下 md5, md5 可以把一个字节数组变成统一长度的字符串,而且这个过程是不可逆的,每一个字节数组最终都会有一个独特的 md5, 但是你无法从 md5 反推我们原来的字符串,现在又冒出来一个问题,字节数组是什么,大家请自行百度,如果全部展开会导致主题不明

我们先来讲 md5,之后再来看看官方代码的意思

md5 这个方法常常用在加密密码和账号,比如你在网站上面注册了一个账号,你的账号和密码通常不会是明文存储在服务器,而是经过加密之后再存到服务器的,这样有一个好处,就是及时服务器的数据库被黑客攻击了,黑客也只能拿到一堆 md5 加密之后的账号和密码,甚至连看 md5 来猜密码的长度都做不到,因为 md5 都是一样长的,这样黑客就没有办法登陆客户的账号,为客户安全更上一层楼

我们现在再来看我们的代码,就能得知,我们实际上是对注意这个函数的参数不是字符串而是字节数组(相当于aardio中的buffer)这一长串字符串进行了一个加密,然后将输出的结果放到我们的输出框 edit 上面

调用的步骤

这个时候我们就能看出来大概的步骤了

  1. 释放 GIL
  2. 新建一个函数用作我们多线程启动
  3. 在函数里面导入我们需要的库(重要)
  4. py3.lock() 在函数内重新加锁
  5. 然后在 lock() 里面写上一个函数,把我们更多的函数内容写在我们这里面
  6. 使用 thread.invoke() 调用,第一个参数传入我们需要调用的函数名字,第二个参数传入

但是非常遗憾,我们现在在子线程当中是无法使用导出 export 功能的,这意味着在多线程里面无法实现之前那样直接在python中修改aardio的窗口,而且也无法完成重定向,点下按钮之后就只能等程序运行结束才会有返回值,如果参数传递过去就会有内存保护机制报错

希望aardio 的作者后期能够支持,如果之后aardio更新了我也会第一时间跟进

代替解决方案

不过虽然没有办法传递我们的aardio窗口,但是单行调用的形式在 aardio 中是可以实现的,就是像这样子直接在 aardio 里书写单行代码

var hashlib = py3.import("hashlib"); 
var md5 = hashlib.md5()
md5.update( raw.buffer("注意这个函数的参数不是字符串而是字节数组(相当于aardio中的buffer)") ); 
winform.edit.print(thread.getId(), tostring(md5.hexdigest()) ); 			

然后在 aardio 中直接操作界面元素

我们举一个新的例子

var os =py3.import("os")
winform.edit.text += os.listdir(".").parseValue()

所以并不推荐在 aardio 中使用 python 的多线程,除非是迫不得已

不过耗时的任务依旧要丢入多线程中防止界面卡死

var code = /*
import threading
import os
import time
import queue
q = queue.Queue()
re_list = []

def waste_time(x):
    thread_id = threading.currentThread()
    print(f"{thread_id.ident} 当前开始运行,即将等待 {x} 秒")
    time.sleep(x)
    print(f'{thread_id.ident} 线程运行结束')
    # 将结果打印到我们的 编辑框上
    q.put(f"{thread_id.ident} 完成")
    return f"{thread_id.ident} 完成"
    
    
    
def start():
    threads = [threading.Thread(target= waste_time, args=(1,)) for _ in range(6)]
    for i in threads:
        i.start()

    for i in threads:
        i.join()
    
    
    while not q.empty():
    	re_list.append(q.get())
    print(re_list)
    return re_list
	*/
py3.exec(code);
/*
要点2、一定要在主线程释放GIL,之后任何python调用都在在py.lock里执行
注意,只有你确实要在多个线程并发调用python时,才需要使用GIL。
*/
py3.releaseThread();

pyThread = function(winform){
	import py3; 
	
	
	
	/*
	要点3、启用GIL以后,任何python调用都必须在py.lock里执行
	python并不支持真正的多线程,所有调用都要加锁互斥运行。
	*/
	py3.lock(
		function(){
			
			return py3.main.start()	
		}
	) 
}


winform.button.oncommand = function(id,event){
   
    var result = thread.invokeAndWait( pyThread,winform );
    console.log(result);
    winform.edit.appendText(result) 
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值