qt 异步ui

2025/2/252525 字

我们知道像这种gui框架都是有一个ui线程用来处理ui交互事件的。

所以一旦你在ui的事件处理方法中调用了耗时特别长的一个函数,ui界面就会卡住。

所以为了防止这种情况发生,在需要执行长时间任务的时候,不能在ui线程执行,而是开一个新的线程去执行。

不过不同的线程我们不知道什么时候结束,也就无法获取这个线程执行结束后得到的返回值。

于是有回调函数的线程就应运而生了,这种方式也被称之异步。

异步

我们先来回顾一下异步调用的特点。

一个典型的异步调用就像下面这样

python
async 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的信号槽机制实现回调函数

python
from 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)  # 发射回调信号,并传递返回值

上面就是使用多线程模拟的异步,还并不是真正的异步,不过有助于我们理解异步操作。

真正的异步

  1. 事件循环(Event Loop) :
  • asyncio的事件循环是异步编程的心脏,它负责调度和执行异步任务。事件循环会持续运行,等待和分发事件,如I/O操作的完成、定时器超时等。
  • 事件循环允许你在代码中挂起(await)当前任务,当某个异步操作(如网络请求或文件读写)需要等待时,事件循环会挂起当前任务,并转而执行其他任务。
  1. 协程(Coroutine) :
  • 协程是一种特殊类型的函数,使用 async def定义。它们在 asyncio中是并发的基本单元。
  • 协程在遇到 await表达式时会暂停执行,将控制权交还给事件循环,允许其他协程运行。当等待的操作完成时,事件循环会恢复协程的执行。
  1. 任务(Task) :
  • 任务是协程的执行单元,用于在事件循环中执行协程。你可以使用 asyncio.create_task()或 asyncio.ensure_future()将协程封装成任务。
  • 任务可以被暂停、恢复和取消,它们是事件循环调度和管理的基本单位。
  1. Future对象 :
  • Future对象代表一个可能尚未完成的异步操作。它提供了一个接口来查询操作的状态(是否完成),以及添加完成时的回调函数。
  • Future对象可以被用来包装低级别的异步操作,使其可以在 asyncio的事件循环中使用。
  1. 非阻塞I/O :
  • asyncio库提供了一组非阻塞的I/O函数,这些函数在执行I/O操作时不会阻塞当前线程,而是立即返回一个 Future对象。
  • 这些非阻塞I/O函数与事件循环紧密集成,当I/O操作完成时,事件循环会通知相应的 Future对象,从而触发等待这个 Future的协程继续执行。
  1. 同步原语 :
  • asyncio提供了一些同步原语,如锁(Lock)、事件(Event)、条件(Condition)和信号量(Semaphore),这些都是异步编程中用于控制并发和同步的高级工具。
  • 这些同步原语都是异步安全的,意味着它们可以在协程中使用,而不会阻塞事件循环。
  1. 异步上下文管理器 :
  • asyncio支持异步上下文管理器(async with),允许你以同步的方式编写异步代码,这使得资源管理更加方便和安全。

asyncio的事件循环和协程共同工作,实现了真正的异步I/O操作。当你在协程中使用 await关键字等待一个异步操作时,事件循环会挂起当前协程,并执行其他协程,直到等待的操作完成。这种方式使得单个线程可以高效地处理多个并发任务,而不需要多线程或多进程的开销。

仔细想来,原来这种真正的异步就是将一个线程也变成一个任务队列,然后其中运行的单元叫做协程。。

果然计算机就是一层层地抽象,

一个cpu核心只能执行线性的代码,但是可以分成不同的进程并发执行,

一个进程只能线性地执行,但是可以分成不同的线程并发执行,

一个线程只能线性地执行,但是可以不成不同的协程并发执行

构建和切换的成本逐渐减低,程序员可以掌控的程度不断提高

到了协程,挂起都是程序员自定义的,一个await就可以挂起了,然后切换到其它协程里面去。

看来异步和同步只是一种概念,既可以使用多线程实现异步,也可以使用协程实现异步,不过协程更方便,毕竟一个线程上的协程是共享一个栈的,多个线程的栈不同,只有堆是独立的。

那io这种需要大量等待的确实用协程合适,多线程没必要,等待又不耗算力。

但是并行计算必然是多线程的,分协程又不能加快速度。

线程并发和协程并发比较

io任务

多线程

python
import 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

多协程

python
import 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次

python
if __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)
plaintext
Total time:  1.0308868885040283
Sum:  1000

目前来看二者差距不多,但是在10000次的时候

多线程

plaintext
总耗时: 2.846590995788574
10000

多协程

plaintext
Total time:  1.097346544265747
Sum:  10000

多线程终究还是被建立线程的成本给拖累了。

但是到了长时间计算的场合又怎么样呢?

python
def long_compute():
    # 模拟计算操作
    sum = 0
    for i in range(10000):
        sum += i
    return sum

1000次

多线程

plaintext
总耗时: 0.4395105838775635
49995000000

多协程

plaintext
Total time:  0.317929744720459
Sum:  49995000000

10000次

多线程

plaintext
总耗时: 4.307144641876221
499950000000

多协程

plaintext
Total time:  3.0482468605041504
Sum:  499950000000

什么,多线程还是输了吗?

一个cpu核心的力量还是有限啊

于是我们的多线程选手召唤出了终极大招多进程(进程池版本)!

python
import 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比不过多协程

💬 评论区

写评论

暂无评论,来发表第一条吧。

© 2026 Corn 的自留地