Python多线程(并行执行)实现

힘센캥거루
2025년 7월 2일(수정됨)
61
python
Python多线程(并行执行)实现-1

用Python进行编程时,在实现像tkinter这样的GUI时会遇到问题。

按下按钮执行特定函数时,GUI也会一起卡住。

因此,我决定用多线程来解决这个问题。

1. 多线程?多进程?

Python多线程(并行执行)实现-2

多线程是在一个进程中同时执行多个工作流(线程)的方法。

例如,当用tkinter创建的GUI在主线程中运行时,如果按钮触发的函数需要很长时间执行,那么GUI也会卡住。

对于在学校工作的人员来说,像UNIV(优尼布)这样的招生程序就是例子。

Python多线程(并行执行)实现-3

打开特定窗口时,原来的窗口会卡住,这对于用户来说非常不方便。

这时,将由按钮点击触发的函数在单独的线程中执行,GUI仍在主线程中运行,其他线程执行函数,因此界面不会卡住。

Python多线程(并行执行)实现-4

相反,多进程是创建和执行完全独立的进程的方式。

线程是在一个进程中共享内存进行执行,而多进程是通过复制进程在独立的内存空间中执行。

可以使用多个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这样可避免麻烦。

选项

说明

示例

target

要执行的目标函数

target=my_function

args

传递给函数的参数(元组)

args=("参数1", 2)

daemon

守护线程的选项。True时,主线程结束时一并结束

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。

函数将简单地利用上述的printHelloprintHi来实现。

如下实现了一个类。

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安装一个库。

这样执行后,窗口将如下显示。

点击按钮时将显示输出。

Python多线程(并行执行)实现-5

即使快速点击按钮,原窗口也不会卡住。

因为窗口在主线程中运行,而打印函数被分到子线程中执行。

通过这种方式,可以使窗口不会卡住,而函数可以继续执行。

5. 实际上这不是多线程吗...?

Python中有一个称为GIL(全局解释器锁)的机制,所以不能在cpu上同时执行多个线程。

据说是为提高Python的速度和开发稳定性而创建的。

但是,运行上述代码时,看起来像是同时执行的。

Python多线程(并行执行)实现-6

这是因为Python代码在一个线程中快速交替执行。

类似于JavaScript的事件循环。

像time.sleep(1)这样的函数不占用cpu,处于等待状态,这段时间内处理其他必要的函数。

通过这种方式,实际上是一个线程,但看起来不止一个。

6. 结语

初学Python时,感觉多线程(多任务)概念有些陌生和困难。

所以简单地只在终端窗口中使用,但进入开发阶段后,终端的黑窗口让我感到困难,于是第一次用customtkinter实现了GUI。

在制作过程中,对多线程有了明确的理解。

编程中,通过解决项目获得的经验,比听讲座或上课更为丰富。

今后我也会继续参与项目开发。

댓글을 불러오는 중...