技术分享 2025-02-08

Python多线程(1)

Python多线程实战笔记

Python 多线程 进程 通信

python多线程实战——进程与线程

在项目开发过程中,总结出关于多线程的知识与经验,供开发者参考

线程 vs 进程

在os课程中,关于线程和进程的区别讲法过于“学院派”,很难直观体会。在实际开发的具体场景中,这种区别其实很明显:

进程--以terminal的视角来看

从一个特别直观的角度出发,在vscode的终端中,不同的终端天然地区分了不同进程(当然一个终端可以有多进程,以及后台进程)

终端

在这个终端中运行 python test.py,就开启了一个python进程;如果点击[+],新建一个终端,重新运行python test.py,开启的是全新的python进程

由这个方式,我受到启发,如果我在python脚本内部,通过os的命令行来运行另外一个python脚本(如 cmd = ["python", "test.py"]),就可以通过一个进程启动另一个进程了

不过,涉及多进程的内容,不在本章节中做过多讨论,上述内容更重要的是和多线程进行区分

线程

线程的启动方式就比较简单了,只需要定义一个函数def xxx_thread(),然后用python的threading库启动就行

def monitor_loop():
    pass 

import threading 
monitor_thread = threading.Thread(target=monitor_loop)
monitor_thread.start()  

...
通过上述方式,就启动了监控线程,自动运行监控循环,同时不会阻塞后续的...代码(主线程运行了monitor_thread.start()后会自动往后走)

通信

进程通信
进程之间的通信是非常麻烦的,不同的进程内部信息不共享,如

# 定义一个数据缓存  
class Cache:
    def __init__(self):
        self._record = {}

    def get(self, key):
        return self._record.get(key)

    def put(self, key, value):
        self._record[key] = value 

CACHE = cache() # 单例  

假设我有一个listener进程,负责监听启动信号req和启动信息meta; 一个worker进程负责工作

如果我想通过cache来传递信息,如:

# listener进程

import subprocess

cache = CACHE

... # 接收信号的功能  
req = message.get('req')
meta = message.get('meta')
if req == 'start':
    cache.put('meta',meta)  
    cmd = ["python", "worker.py"]
    subprocess.run(cmd)

# worker进程

meta = cache.get('meta')
print(meta)

... # 工作的功能  
会发现,worker的meta是None,因为worker是通过subprocess.run(cmd)启动的,cmd是全新的进程,与listener进程不共享内存

线程通信
而线程不存在这个问题,如:

# listener进程

cache = CACHE

... # 接收信号的功能  
req = message.get('req')
meta = message.get('meta')

def worker():
    meta = cache.get('meta')
    print(meta)

    ... # 工作的功能  

if req == 'start':
    cache.put('meta',meta)  
    worker_thread = threading.Thread(target=worker)
    worker_thread.start()
发现worker能正常获取到meta,因为worker和listener在同一个进程中,共享内存

多线程通信
不同的线程,可以通过主线程中的共享变量实现通信,如定义的缓存CACHE:

# listener进程

cache = CACHE

... # 接收信号的功能  
req = message.get('req')
meta = message.get('meta')

def worker1():
    meta = cache.get('meta')

    ... # 工作的功能  

def worker2():
    cache.get('meta')

    ... # 工作的功能  

if req == 'start':
    cache.put('meta',meta)  
    worker_thread1 = threading.Thread(worker1)
    worker_thread2 = threading.Thread(worker2)
    worker_thread1.start()
    worker_thread2.start()
发现worker1和worker2都能正常获取到meta,因为它们在同一个进程中,共享内存

线程的创建与销毁

在 Python 中,可以通过threading.Thread对象来创建新线程。每个线程都是由主进程统一调度和管理的,因此线程的生命周期是受到进程生命周期的约束的。当进程结束时,其下所有线程都会被强制终止。

线程的创建 创建线程时,需要指定线程要运行的目标函数(target),并可通过args/kwargs传入参数。例如:

import threading

def my_worker(name):
    print(f"{name} is working...")

# 创建线程
t = threading.Thread(target=my_worker, args=("worker1",))
t.start()
通过.start()方法启动线程,主线程会异步地启动新线程来执行目标函数。

线程的结束 1. 主动结束:线程的函数执行完毕,线程就会自动退出,无需手动销毁。 2. 被动结束:如果主进程退出,其下所有子线程会被强制关闭(包括未完成的daemon线程)。 3. 守护线程(daemon):可以设置线程为守护线程daemon=True,此时主进程退出时守护线程会直接随之结束;而非守护线程会阻止主进程退出,直到它们运行结束。

t = threading.Thread(target=my_worker, args=("daemon_worker",), daemon=True)
t.start()

注意,线程没有t.stop()方法,强行中断线程可能导致资源的泄露(如文件写到一半被中断,且仍然保持打开状态)