
用Python进行编程时,在实现像tkinter这样的GUI时会遇到问题。
按下按钮执行特定函数时,GUI也会一起卡住。
因此,我决定用多线程来解决这个问题。
1. 多线程?多进程?

多线程是在一个进程中同时执行多个工作流(线程)的方法。
例如,当用tkinter创建的GUI在主线程中运行时,如果按钮触发的函数需要很长时间执行,那么GUI也会卡住。
对于在学校工作的人员来说,像UNIV(优尼布)这样的招生程序就是例子。

打开特定窗口时,原来的窗口会卡住,这对于用户来说非常不方便。
这时,将由按钮点击触发的函数在单独的线程中执行,GUI仍在主线程中运行,其他线程执行函数,因此界面不会卡住。

相反,多进程是创建和执行完全独立的进程的方式。
线程是在一个进程中共享内存进行执行,而多进程是通过复制进程在独立的内存空间中执行。
可以使用多个CPU核心,所以更适合大量计算的工作,但在GUI程序中需要进程间通信,所以不如线程使用简单。
分类 | 多线程 | 多进程 |
|---|---|---|
执行方式 | 单个进程内的多个线程 | 多个独立进程 |
内存 | 共享内存空间 | 独立内存空间(复制) |
CPU利用 | 由于GIL(全局解释器锁),对CPU密集型任务有限制 | 可以使用多个CPU核心 |
与GUI的关系 | 适合防止GUI卡住 | 与GUI是独立进程,需要通信 |
使用示例 | tkinter按钮点击后执行函数 | 图像处理、大规模计算等 |
Python能够进行多进程是因为可以使用multiprocessing库通过复制或创建新的进程来执行。
但由于内存是进程独立的,要共享数据需要使用Queue、Pipe、共享内存等。
因此,对于像GUI程序这样用户界面不能卡住的情况,使用多线程,而对于大规模计算任务,合理使用多进程比较好。
2. 多线程代码示例
假设有如下代码。
这段代码打印指定次数的hello和hi。
import time
def printHello(num):
for i in range(num):
print(f'hello-{i}')
time.sleep(1)
def printHi(num):
for i in range(num):
print(f'hi-{i}')
time.sleep(1)
printHello(3)
printHi(2)
像最下面那行代码那样执行时,Hello会先打印3次,然后Hi会打印2次。
由于Python是同步执行的函数,直到一个函数完成,才会执行下一个函数。
hello-0
hello-1
hello-2
hi-0
hi-1
现在,用多线程来实现这一点。
printHello和printHi的内容与上面相同,代码注释中有简单说明。
import threading
import time
...
hello = threading.Thread(target=printHello, args=(3, ), daemon=True)
hello.start()
hi = threading.Thread(target=printHi, args=(2, ), daemon=True)
hi.start()
hello.join()
hi.join()
print('작업종료')
这样执行时,hello和hi会以1秒间隔同时输出。
在最后一次hello输出后,打印任务结束。
hello-0hi-0
hello-1hi-1
hello-2
작업종료
这里线程的参数中重要的部分如下。
daemon通常为true这样可避免麻烦。
选项 | 说明 | 示例 |
|---|---|---|
| 要执行的目标函数 |
|
| 传递给函数的参数(元组) |
|
| 守护线程的选项。 |
|
3. 为什么说使用futures代替threading?
虽然使用threading不是很难,但需要处理daemon、join等过程。
从Python 3.2开始,支持concurrent.futures库,使用此库实现可以更简单。
import time
from concurrent.futures import ThreadPoolExecutor
def printHello(num, name):
for i in range(num):
print(f'{name}-hello-{i}')
time.sleep(1)
def printHi(num, name):
for i in range(num):
print(f'{name}-hi-{i}')
time.sleep(1)
executor = ThreadPoolExecutor(max_workers=2)
future1 = executor.submit(printHello, 5, "Alice")
future2 = executor.submit(printHi, 5, "Bob")
future1.result()
future2.result()
print("所有任务完成")
只需将函数及该函数内部的参数按顺序传递给submit的参数即可。
在submit时,函数执行开始,而调用result时,会等待函数结束,并可以获取返回值。
如果不调用result,主线程不会等待,程序可能会提前结束。
基本上内部已声明daemon,因此无需额外声明。
比threading更直观。
为防止内存泄漏,可以使用with语句来封装,如下。
with ThreadPoolExecutor(max_workers=2) as executor:
future1 = executor.submit(printHello, 5, "Alice")
future2 = executor.submit(printHi, 5, "Bob")
future1.result()
future2.result()
with语句会自动调用executor.shutdown(wait=True)安全终止线程。
4. 示例:同时执行GUI和函数
现在让我们同时执行一个简单的GUI和函数。
将使用customtkinter作为GUI。
函数将简单地利用上述的printHello和printHi来实现。
如下实现了一个类。
from concurrent.futures import ThreadPoolExecutor
import time
import customtkinter as ctk
class helloHiWindow:
def __init__(self):
self.hello = 5
self.hi = 6
self.app = ctk.CTk()
self.executor = ThreadPoolExecutor(max_workers=2)
self.customWindow()
def run(self):
self.app.mainloop()
def customWindow(self):
self.app.title("Hello! Hi!")
self.app.geometry("300x200")
button1 = ctk.CTkButton(self.app, text="Hello!", height=10, command=self.startHello)
button1.pack(pady=20)
button2 = ctk.CTkButton(self.app, text="Hi!", height=10, command=self.startHi)
button2.pack(pady=20)
def startHello(self):
hello = self.executor.submit(self.printHello)
def startHi(self):
hi = self.executor.submit(self.printHi)
def printHello(self):
for i in range(self.hello):
print(f'hello-{i}')
time.sleep(1)
def printHi(self):
for i in range(self.hi):
print(f'hi-{i}')
time.sleep(1)
if __name__ == "__main__":
app = helloHiWindow()
app.run()如果使用Macbook,可能需要通过brew install python-tk安装一个库。
这样执行后,窗口将如下显示。
点击按钮时将显示输出。

即使快速点击按钮,原窗口也不会卡住。
因为窗口在主线程中运行,而打印函数被分到子线程中执行。
通过这种方式,可以使窗口不会卡住,而函数可以继续执行。
5. 实际上这不是多线程吗...?
Python中有一个称为GIL(全局解释器锁)的机制,所以不能在cpu上同时执行多个线程。
据说是为提高Python的速度和开发稳定性而创建的。
但是,运行上述代码时,看起来像是同时执行的。

这是因为Python代码在一个线程中快速交替执行。
类似于JavaScript的事件循环。
像time.sleep(1)这样的函数不占用cpu,处于等待状态,这段时间内处理其他必要的函数。
通过这种方式,实际上是一个线程,但看起来不止一个。
6. 结语
初学Python时,感觉多线程(多任务)概念有些陌生和困难。
所以简单地只在终端窗口中使用,但进入开发阶段后,终端的黑窗口让我感到困难,于是第一次用customtkinter实现了GUI。
在制作过程中,对多线程有了明确的理解。
编程中,通过解决项目获得的经验,比听讲座或上课更为丰富。
今后我也会继续参与项目开发。
댓글을 불러오는 중...