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

힘센캥거루
2025년 7월 2일(수정됨)
6
66
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。

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

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

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

관련 글

学校事务自动化——利用 AI 检查学生综合素质评价(生记簿)科目别“细部能力及特长事项”
学校事务自动化——利用 AI 检查学生综合素质评价(生记簿)科目别“细部能力及特长事项”
如果要在学校工作里选出一项最无意义、最辛苦、最无聊的,我会选生记簿检查。在初中,学生综合素质评价(生活记录簿)并不那么重要,但在高中它与升学直接相关,因此极其重要。问题在于,这样的生记簿检查最终找的无非就是简单的错别字、禁止填写用语、拼写等。这篇文章就是从这样的疑问开始的。现在这种简单检查,是不是已...
从零构建中学习 LLM 第7章读书心得与挑战回顾
从零构建中学习 LLM 第7章读书心得与挑战回顾
第7章的内容是让模型遵循指令进行微调的过程。也就是让它针对某个问题给出我们期望的回答。果然,最需要的还是数据。1. 指令微调步骤这里的核心是准备好问答数据集,用作输入-输出对来进行训练。这就叫做提示(prompt)风格。其他部分就像之前的流程一样,对内容进行分词(tokenize)、训练和评估,过程...
从零开始构建中学习 LLM 第 6 章读后感
从零开始构建中学习 LLM 第 6 章读后感
第 6 章是为分类进行微调。作为例子给出的任务是构建垃圾邮件分类器。垃圾邮件分类器需要判断一封邮件是不是垃圾邮件,因此输出结果要是类似 0、1 这样的值。1. 微调的顺序微调的过程和训练模型的过程很相似。准备数据集,加载权重值,然后进行训练和评估。稍微不同的一点是,会有一个把输出层映射到 0(非垃圾...
从零开始构建中学习 LLM 第5章读书后记
从零开始构建中学习 LLM 第5章读书后记
今天是12月14日。其实挑战期已经过去整整两周了,但也不能因此就放弃写后记。像这样留下的 TIL(Today I Learned),以后都会变成自己的血和肉。这次打算比起代码本身,更专注在“意义”上来写一写。1. 模型的损失计算这一部分讲的是,在构建好 GPT 模型之后,用什么方式来计算损失。GPT...
从零开始动手实现 LLM 第4章读书心得
从零开始动手实现 LLM 第4章读书心得
今天是11月26日,如果每天读一章并看完的话,这次挑战就算成功。在老大和老二的各种干扰下,不知道能不能做到。1. Dummy Transformer在实现 GPT 模型的过程中,看到是从 PyTorch 里拿来一个 Transformer 的 dummy 模块用的。一查才发现,在 PyTorch 的...
通过从头构建学习的LLM第3章读后感
通过从头构建学习的LLM第3章读后感
我在MacBook上泼了一大杯水后,崩溃了,浪费了大约3-4天。现在回想起来,反正MacBook已经坏了,应该想着送修,干点别的事情。无论如何,虽然有点晚了,但我觉得必须坚持到底,所以留下了第3章的读后感。1. 注意力机制第3章...

댓글을 불러오는 중...