目录
1. 引言
GUI应用程序在回调或异步调用流中工作,这在架构中是事件驱动的。维护事件循环,并逐个执行所有已注册/计划的回调函数。需要处理的事件必须向事件循环管理器注册。回调函数与事件相关联。事件循环中的事件通过执行关联的回调函数来执行。
针对GUI事件调用GUI回调。从事件回调进行阻塞I/O将延迟GUI事件的处理,从而导致GUI冻结。本文讨论如何在GUI框架中以非冻结GUI的方式提供阻塞的外部I/O。
2. 问题
如何在GUI相关应用中阻止外部I/O?
3. 场景
GUI应用程序在事件回调程序流中工作,而不是顺序程序流,即“C编程”。具有关联回调函数的事件需要向事件循环管理器注册。事件触发后,事件循环调度程序/管理器将调用关联的回调函数。从任何事件回调函数进行阻塞I/O调用都会冻结GUI,因为程序的执行不会返回到事件循环。禁止事件循环管理器从事件循环调度任何GUI事件。
4. 解决方案
在这个领域有两件事:
- I/O通过文件描述符(即套接字)发生。
- 与所有其他事件类型一样,还有一个计时器事件,该事件在超时到期时触发。
建议在‘Timer
’事件回调中进行I/O操作,其中'select '、I/O描述符多路复用器、api用于以非阻塞方式检查文件描述符集/列表上的读取或写入活动。一旦'select' api返回而没有超时,就会发生I/O。超时将为零,使计时器事件回调完全不阻塞。
5. 外部基准电压源
'select'' API在未提供timeout参数时阻塞,否则在超时时不阻塞。'select' api设备驱动程序实现可以在Linux设备驱动程序第3版一书中找到。在Oreilly出版物下出版。
在“高级字符驱动程序操作”一章的“轮询和选择”小节下。而Python实现文档可以在docs.python.org上找到。
“GUI编程”事件机制可以在Xlib编程手册的“事件”一章中找到。
在Python中,“事件循环”是主题“异步i/o”中的一个子部分。
6. 设计
设计为在事件循环管理器中注册计时器事件。定时器事件回调函数将在非阻塞超时模式下对I/O文件描述符执行“select”。如果为I/O设置了描述符,则将发生I/O。计时器回调函数将返回事件循环处理或零超时。
7. 实验输出
我们有一个场景,当Blender(一个3D GUI建模工具)需要运行GUI工具外部的Python程序并作为独立进程执行时。默认情况下,Blender支持“应用程序内”Python解释器。请遵循下图:
Blender进程中/应用程序Python IDE
我们需要Blender Python支持作为进程/应用程序Python应用程序。像这样:
Python客户端通过套接字连接连接到运行Python服务器代码的Blender。
Python服务器代码向事件循环管理器注册Timer事件。在计时器事件回调中,Python服务器通过'select ' api调用检查I/O。如果发生超时或设置了描述符(数据到达),则在处理请求后将返回计时器事件回调。客户端和服务器Python代码。
$cat blender_client2.py
#!/usr/bin/env python
#blender_client.py script1.py script2.py
#developed at Minh, Inc. https://youtube.com/@minhinc
import sys,time,select
PORT = 8081
HOST = "localhost"
def main():
import sys
import socket
if len(sys.argv)<2:
print(f'---usage---\npython3 blender_client.py blenderpythonscript.py blenderpythonscript2.py ..')
sys.exit(-1)
clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
clientsocket.connect((HOST, PORT))
except Exception as exc:
print(f'E Error in connection with server, probably try after sometime/minutes... Exception type -
> {type(exc)=}')
sys.exit(-1)
else:
print(f'I blender_client connection to server successful')
filestosend=' '.join(sys.argv[1:])
print(f'sending file(s) -> {filestosend} to the server')
clientsocket.sendall(filestosend.encode("utf-8") + b'\x00')
print(f'I blender_client message sent successfully, waiting for response..')
while True:
messagerecved=clientsocket.recv(4096)
if not messagerecved:
print(f'Empty message received, sleeping for 10 secs...')
time.sleep(10)
else:
print(f'Message received {messagerecved=}, exiting...')
clientsocket.close()
break
if __name__ == "__main__":
main()
$cat blender_server2.py
#blender --python blender_server.py
#developed at Minh, Inc. https://youtube.com/@minhinc
import socket,time,select,re,datetime
import bpy
PORT = 8081
HOST = "localhost"
PATH_MAX = 4096
def execfile(filepath):
import os
global_namespace = {
"__file__": filepath,
"__name__": "__main__",
}
with open(filepath, 'rb') as file:
exec(compile(file.read(), filepath, 'exec'), global_namespace)
def main():
global serversocket,read_list,file_list,connection,result_list
file_list=[]
result_list=[]
connection=None
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind((HOST, PORT))
serversocket.listen(5) #accept upto 6 connect and messages
print("Listening on %s:%s" % (HOST, PORT))
read_list=[serversocket]
def handle_data():
global file_list,connection,result_list
timeout=20
def send_data():
nonlocal timeout
print(f'I blender_server executing file {file_list[0]} full {file_list=} ')
try:
execfile(file_list[0])
print(f'executed successfully {file_list[0]=}')
result_list.append(f'{file_list[0]} - success')
except Exception as exc:
print(f'Error while executing {file_list[0]=} {exc=}')
result_list.append(f'{file_list[0]} - failed exception {exc}')
file_list[0:1]=[]
timeout=2
if file_list:
send_data()
else:
if connection:
connection.sendall('\n'.join(result_list).encode('utf-8') + b'\x00')
print("response ",'\n'.join(result_list)," sent to client")
connection.close()
connection=None
result_list=[]
readable,writable,errored=select.select(read_list,[],[],0.0)
print(f'E handle_data() {(readable,writable,errored)=} {read_list=} at time ->
{datetime.datetime.now():%H:%M:%S}')
for s in readable:
if s in read_list:
connection, address = serversocket.accept()
print(f'I blender_server connection accepted {connection=} {address=}')
file_list = re.split(r'\s+',re.split(b'\x00',connection.recv(PATH_MAX))[0].decode())
print(f'I blender_server data received {file_list=}')
send_data()
print(f'handle_data, returning {timeout} second timeout..')
return timeout
if __name__ == "__main__":
main()
bpy.app.timers.register(handle_data)
脚本可以在两个终端中执行,如下所示:
1、第一个终端
Blender — Python blender_server.py
2、第二终端
python3 blender_client.py <pythonprogram1> <pythonprogram2> <pythonprogram3>
下图用于打开两个终端:
客户端和服务器各有双终端
7.1 程序说明
客户端程序接受多个Python程序作为命令行参数。程序名称以string形式加入并发送到服务器。服务器解析请求并逐个处理每个Python文件。每次它从计时器事件循环返回时,事件循环管理器将有机会处理其他事件。
Python程序以串联string形式发送到Python服务器。服务器进程一个接一个。在每次文件处理后返回事件循环。
7.2 视频
关于为客户端和服务器分叉终端的视频。
视频——从客户端到服务器触发python脚本。
7.3 立方体增删GIF动画
通过客户端应用程序触发的脚本添加和删除多维数据集的GIF动画
7.4 多维数据集添加和删除代码
$ cat cubeadd_y.py
import bpy
import bmesh
import mathutils
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=4)
mesh = bpy.data.meshes.new('Basic_Cube')
bm.to_mesh(mesh)
mesh.update()
bm.free()
basic_cube = bpy.data.objects.new("Basic_Cube", mesh)
basic_cube.matrix_world.translation += basic_cube.matrix_world.to_3x3() @
mathutils.Vector((0.0,6.0,0.0))
bpy.context.collection.objects.link(basic_cube)
$ cat cubeadd_x.py
import bpy
import bmesh
import mathutils
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=4)
mesh = bpy.data.meshes.new('Basic_Cube')
bm.to_mesh(mesh)
mesh.update()
bm.free()
basic_cube = bpy.data.objects.new("Basic_Cube", mesh)
basic_cube.matrix_world.translation += basic_cube.matrix_world.to_3x3() @ mathutils.Vector((-
6.0,0.0,0.9))
bpy.context.collection.objects.link(basic_cube)
$ cat cubedelete.py
import bpy
import mathutils
try:
cube = bpy.data.objects['Cube']
bpy.data.objects.remove(cube, do_unlink=True)
except:
print("Object bpy.data.objects['Cube'] not found")
bpy.ops.outliner.orphans_purge()
8. 它是如何工作的?
两个Python程序客户端和服务器通过“套接字”进程间通信相互交互。套接字也可用于计算机间通信。IP地址需要是服务器的真实IP地址。Blender以脚本模式启动,并以' — python'作为命令行参数。Blender在主线程中启动python程序。Python程序不执行任何任务,而是注册一个“计时器事件”以事件循环代码。
交互流的活动图
'bpy.app.timers.register(handle_data)'将'handle_event'作为回调函数传递。事件回调函数'handle_data'在主循环中调用,它使用'select'作为I/O多路复用器,以非阻塞模式处理I/O。一旦连接到达“读取描述符已设置”,就会读取并处理连接请求。如果有多个文件,Timer回调将返回(到事件主循环),超时为0秒。在这里,2秒用于使解释更加直观。返回到每个Python脚本文件处理之间的事件循环,使事件循环管理器有机会执行其他GUI事件,使GUI看起来是交互式的。
9. 进一步增强
客户端Python脚本可以通过IDE编辑器进行编辑。编辑器将具有GUI按钮选项和上下文菜单选项来执行脚本。
建议的IDE编辑器,带有单独的工具栏按钮和上下文菜单来执行脚本
与服务器的数据/字符串通信将显示在“docket”窗口中。
10. 进一步的研究
- UNIX环境中的高级编程,W. Richard Stevens。Addison-Wesley专业计算系列
- TCP/IP图解,W. Richard Stevens:协议第1卷,实现第2卷,TCP for Transactions、HTTP、NNTP和UNIX域协议第3卷
https://www.codeproject.com/Articles/5375662/Non-blocking-GUI-while-Serving-Blocking-External-I