qt 异步ui
我们知道像这种gui框架都是有一个ui线程用来处理ui交互事件的。
所以一旦你在ui的事件处理方法中调用了耗时特别长的一个函数,ui界面就会卡住。
所以为了防止这种情况发生,在需要执行长时间任务的时候,不能在ui线程执行,而是开一个新的线程去执行。
不过不同的线程我们不知道什么时候结束,也就无法获取这个线程执行结束后得到的返回值。
于是有回调函数的线程就应运而生了,这种方式也被称之异步。
异步
我们先来回顾一下异步调用的特点。
一个典型的异步调用就像下面这样
pythonasync def long_time_task():
sleep(long_time)
return "result"
async def caller():
result = await long_time_task()
print(result)
上面我们就有一个长时间任务和一个调用者caller
两者都是异步方法,使用async标识,async就只是个标识符,并不会起到什么真正的作用,只是标识某个方法是异步方法,可以对它使用一些异步的操作。
上面真正有用的是await
虽然看字面意思似乎是caller会阻塞在这里,一直等到long_time_task返回后才会继续向后执行
对了,但是不完全对。
有await在,caller确实会等到long_time_task返回后才继续执行,但是并不会阻塞这话听着挺怪的,等待又不阻塞?
如何才能做到呢?
现实生活中的例子
当你委托一个人办事之后,有两种方式得到它的结果,一种是什么都不做,就一直看着他把事做完,另一种就是让他做完后把结果告诉你。
第二种方式就是我们想要的效果,等待但不阻塞。就是在等待的过程中如果你有其他事情你大可去做,而不是只能等到委托人的结果出来才能做其他事。
要实现这种效果,首先需要并发,因为你和委托人可以同时做不同的事情。
其次,还需要回调函数,也就是委托人在完成工作后,需要有一个手段将结果返回给你同时可以让你继续之前的工作。
可以通过Qt的信号槽机制实现回调函数
pythonfrom PyQt6.QtCore import QThread, pyqtSignal
# 定义任务执行器类,用于异步执行耗时函数
class TaskRunner(QThread):
task_completed = pyqtSignal(object) # 定义信号,可以传递任意类型的返回结果
def __init__(self, func=None, *args, **kwargs):
super().__init__()
self.func = func
self.args = args
self.kwargs = kwargs
def run(self):
if self.func:
try:
result = self.func(*self.args, **self.kwargs) # 执行函数并获取返回值
except Exception as e:
result = e # 捕获函数执行过程中出现的异常
self.task_completed.emit(result) # 发射回调信号,并传递返回值
上面就是使用多线程模拟的异步,还并不是真正的异步,不过有助于我们理解异步操作。
真正的异步
- 事件循环(Event Loop) :
asyncio的事件循环是异步编程的心脏,它负责调度和执行异步任务。事件循环会持续运行,等待和分发事件,如I/O操作的完成、定时器超时等。- 事件循环允许你在代码中挂起(
await)当前任务,当某个异步操作(如网络请求或文件读写)需要等待时,事件循环会挂起当前任务,并转而执行其他任务。
- 协程(Coroutine) :
- 协程是一种特殊类型的函数,使用
async def定义。它们在asyncio中是并发的基本单元。 - 协程在遇到
await表达式时会暂停执行,将控制权交还给事件循环,允许其他协程运行。当等待的操作完成时,事件循环会恢复协程的执行。
- 任务(Task) :
- 任务是协程的执行单元,用于在事件循环中执行协程。你可以使用
asyncio.create_task()或asyncio.ensure_future()将协程封装成任务。 - 任务可以被暂停、恢复和取消,它们是事件循环调度和管理的基本单位。
- Future对象 :
Future对象代表一个可能尚未完成的异步操作。它提供了一个接口来查询操作的状态(是否完成),以及添加完成时的回调函数。Future对象可以被用来包装低级别的异步操作,使其可以在asyncio的事件循环中使用。
- 非阻塞I/O :
asyncio库提供了一组非阻塞的I/O函数,这些函数在执行I/O操作时不会阻塞当前线程,而是立即返回一个Future对象。- 这些非阻塞I/O函数与事件循环紧密集成,当I/O操作完成时,事件循环会通知相应的
Future对象,从而触发等待这个Future的协程继续执行。
- 同步原语 :
asyncio提供了一些同步原语,如锁(Lock)、事件(Event)、条件(Condition)和信号量(Semaphore),这些都是异步编程中用于控制并发和同步的高级工具。- 这些同步原语都是异步安全的,意味着它们可以在协程中使用,而不会阻塞事件循环。
- 异步上下文管理器 :
asyncio支持异步上下文管理器(async with),允许你以同步的方式编写异步代码,这使得资源管理更加方便和安全。
asyncio的事件循环和协程共同工作,实现了真正的异步I/O操作。当你在协程中使用 await关键字等待一个异步操作时,事件循环会挂起当前协程,并执行其他协程,直到等待的操作完成。这种方式使得单个线程可以高效地处理多个并发任务,而不需要多线程或多进程的开销。
仔细想来,原来这种真正的异步就是将一个线程也变成一个任务队列,然后其中运行的单元叫做协程。。
果然计算机就是一层层地抽象,
一个cpu核心只能执行线性的代码,但是可以分成不同的进程并发执行,
一个进程只能线性地执行,但是可以分成不同的线程并发执行,
一个线程只能线性地执行,但是可以不成不同的协程并发执行
构建和切换的成本逐渐减低,程序员可以掌控的程度不断提高
到了协程,挂起都是程序员自定义的,一个await就可以挂起了,然后切换到其它协程里面去。
看来异步和同步只是一种概念,既可以使用多线程实现异步,也可以使用协程实现异步,不过协程更方便,毕竟一个线程上的协程是共享一个栈的,多个线程的栈不同,只有堆是独立的。
那io这种需要大量等待的确实用协程合适,多线程没必要,等待又不耗算力。
但是并行计算必然是多线程的,分协程又不能加快速度。
线程并发和协程并发比较
io任务
多线程
pythonimport threading
import time
sum = 0
def long_io():
# 模拟IO操作,耗时1秒
time.sleep(1)
return 1
def caller(i):
global sum
# 调用long_io和long_compute函数
result = long_io()
# result = await long_compute()
sum += result
1000次
python
if __name__ == '__main__':
start_time = time.time()
# 创建10000个线程
threads = []
for i in range(10000):
t = threading.Thread(target=caller, args=(i,))
threads.append(t)
t.start()
# 等待所有线程结束
for t in threads:
t.join()
end_time = time.time()
print("总耗时:", end_time - start_time)
print(sum)
plaintext总耗时: 1.1328620910644531
1000
多协程
pythonimport asyncio
import time
sum = 0
async def long_io():
# 模拟IO操作,耗时1秒
await asyncio.sleep(1)
return 1
async def caller(i):
global sum
# 调用long_io和long_compute函数
result = await long_io()
# result = await long_compute()
sum += result
1000次
pythonif __name__ == '__main__':
start_time = time.time()
loop = asyncio.get_event_loop()
# 添加5个caller任务
tasks = [asyncio.ensure_future(caller(i)) for i in range(1000)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
print("Total time: ", time.time() - start_time)
print("Sum: ", sum)
plaintextTotal time: 1.0308868885040283
Sum: 1000
目前来看二者差距不多,但是在10000次的时候
多线程
plaintext总耗时: 2.846590995788574
10000
多协程
plaintextTotal time: 1.097346544265747
Sum: 10000
多线程终究还是被建立线程的成本给拖累了。
但是到了长时间计算的场合又怎么样呢?
pythondef long_compute():
# 模拟计算操作
sum = 0
for i in range(10000):
sum += i
return sum
1000次
多线程
plaintext总耗时: 0.4395105838775635
49995000000
多协程
plaintextTotal time: 0.317929744720459
Sum: 49995000000
10000次
多线程
plaintext总耗时: 4.307144641876221
499950000000
多协程
plaintextTotal time: 3.0482468605041504
Sum: 499950000000
什么,多线程还是输了吗?
一个cpu核心的力量还是有限啊
于是我们的多线程选手召唤出了终极大招多进程(进程池版本)!
pythonimport concurrent.futures
import time
def long_compute():
# 模拟计算操作
sum_local = 0
for i in range(10000):
sum_local += i
return sum_local
def caller():
result = long_compute()
return result
if __name__ == '__main__':
start_time = time.time()
# 使用进程池执行任务
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(caller) for _ in range(1000)]
# 收集所有结果
results = [future.result() for future in futures]
# 累加所有结果
total_sum = sum(results)
end_time = time.time()
print("总耗时:", end_time - start_time)
print("总和:", total_sum)
4核 1000 次
plaintext总耗时: 0.2774841785430908
总和: 49995000000
4核 10000 次
plaintext总耗时: 2.256455659866333
总和: 499950000000
遥遥领先
不过有个地方需要注意的是,多进程就不能用全局变量收集result了
因为每个进程的pcb不共享,全局变量自然也不共享,那最终只会打印主进程sum的初始值,也就是0。
那多线程看来是有点幽默的,感觉没啥用了,拼计算比不过多进程,io比不过多协程
💬 评论区
暂无评论,来发表第一条吧。