多线程
进程和线程
进程和线程都是操作系统中关于任务调度和并发执行的核心概念。它们是操作系统为管理、执行和调度应用程序所使用的基本构建块。
进程是一个独立运行的程序实例,拥有自己的独立内存空间和系统资源。从操作系统的视角看,进程是一个执行中的程序和其状态的组合,包括程序计数器、寄存器、虚拟内存等。线程,是进程内部的一个单独的执行流程。线程在进程内共享同一地址空间和资源,但拥有自己的调用栈、程序计数器和寄存器状态。一个进程可以有多个线程。无论进程还是线程都是由操作系统来调度的。每个进程有自己的独立地址空间,但线程没有。在多核或多处理器系统中,多个线程可以不同的 CPU 内核并行执行。
简单来说:一个应用程序至少有一个进程,一个进程至少有一个线程。
我们之前编写的简单程序,都是只有一个进程一个线程的。接下来我们会讨论一下如何开启多个线程和进程。
Python 中的多线程多少有些尴尬,上文提到:“在多核或多处理器系统中,多个线程可以不同的 CPU 内核并行执行。” 是在一般情况下。但是 Python 程序却做不到这一点:Python 程序在多线程下也无法做到多 CPU 并行运行,我们会在下文讨论具体原因。虽然,不能让多 CPU 并行工作,好在多线程还可以让不同的外设读写并行运行,比如同时访问多个文件、数据库、网页等。所以在过去,Python 的多线程主要用于支持并发 I/O。但现在,Python 中有了异步 I/O,多线程连这点优势也没有了。
尽管如此,多线程作为编程语言一个极为重要的概念,仍然值得我们深入研究一下。
threading 模块
Python提供了多种方法来创建和管理多线程,其中最常见和直接的方法是使用标准库中的 threading 模块。
创建线程
使用 threading.Thread 类可以创建一个新的线程。最常见的方法是提供一个函数作为 Thread 类的构造函数的 target 参数。当线程启动时,这个函数或方法会被调用。使用 Thread 对象的 start() 方法可以启动线程,线程启动的同时,开始运行 target 指向的函数。如果需要等待一个线程完成任务,再继续后续程序,可以使用 Thread 对象的 join() 方法等待线程运行结束:
import threading
def print_numbers():
for i in range(5):
print(i)
# 创建线程
thread = threading.Thread(target=print_numbers)
# 启动线程
thread.start()
# 等待线程结束
thread.join()
线程的命名和标识
我们可以在创建线程时使用 name 参数为其命名,然后使用 threading.name 属性获取一个线程的名称。每个线程还有一个唯一的标识,可以使用 threading.get_ident() 获取。
import threading
import time
def worker():
# 获取当前线程的名称
current_thread_name = threading.current_thread().name
# 获取当前线程的标识
thread_ident = threading.get_ident()
print(f"{current_thread_name} (ID: {thread_ident}) 开始")
time.sleep(2)
print(f"{current_thread_name} (ID: {thread_ident}) 结束")
# 创建两个线程,并给它们命名
thread1 = threading.Thread(target=worker, name="线程1")
thread2 = threading.Thread(target=worker, name="线程2")
thread1.start()
thread2.start()
thread1.join()
thread2.join()
# 输出类似:
# 线程1 (ID: 140173712739104) 开始
# 线程2 (ID: 140173711678240) 开始
# 线程1 (ID: 140173712739104) 结束
# 线程2 (ID: 140173711678240) 结束
给线程命名和标识不但可以提高代码可读性和可维护性,还可以方便我们管理和控制线程。如果出现问题,把线程名和标识记录下来,也能帮助我们调试查找问题。
线程局部数据
每个线程可以拥有其自己的数据实例,独立于其他线程。这是通过 threading.local() 来实现的。线程局部数据在某些应用中很有用,例如数据库连接、请求上下文等。
以下是一个使用线程局部数据的简单示例:
import threading
# 创建线程局部数据
local_data = threading.local()
def display_data():
try:
value = local_data.value
except AttributeError:
print("没有数据")
else:
print(f"数据是 {value}")
def worker(number):
# 每个线程根据输入参数设置一个线程局部变量
local_data.value = number
display_data()
# 创建两个线程
thread1 = threading.Thread(target=worker, args=(1,))
thread2 = threading.Thread(target=worker, args=(2,))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
# 在主线程中显示线程局部数据
display_data()
# 输出:
# 数据是 1
# 数据是 2
# 没有数据
在上述代码中,我们首先定义了一个名为 display_data 的函数,用于显示当前线程的局部数据。然后,我们有一个 worker 函数,它是被多个线程运行的函数。worker 函数有输入参数,创建线程时,可以通过 threading.Thread 函数的 args 参数 ,把参数传递给 worker 函数。程序的每个线程,都在 worker 函数中设置了线程局部数据 local_data.value。尽管所有线程都使用相同的名字 "value" 来访问它们的线程局部数据,但每个线程都有其自己的独立数据实例,每个线程会维护各自不同的 value 的值。
主线程并没有设置 local_data.value,如果我们在主线程中尝试访问它,程序会抛出一个 AttributeError 异常。
守护线程
守护线程(Daemon Thread)是一个在后台运行的线程,不与用户直接交互。当主程序结束时,所有守护线程都会被自动终止,不论它们是否正在工作。这与“常规”线程或“用户”线程相反,常规线程在主程序结束后会继续执行直至线程自己结束。
守护线程通常用于执行后台任务,例如垃圾回收、日志管理、监控、自动存档等。其它线程无法使用 join() 函数等待守护线程结束,因为守护线程通常不应该自己结束,而是应该等到主程序结束时被自动关闭。如果在守护线程中,再创建一个新线程,新线程被称为“子线程”,它会默认继承其父线程的守护线程状态。
我们可以通过设置线程对象的 daemon 属性来使线程变为守护线程。
import threading
import time
# 定义守护线程执行的函数
def daemon_thread():
while True:
print("守护线程正在运行...")
time.sleep(1)
# 创建守护线程
# 推荐方式 1:在创建时指定
d_thread = threading.Thread(target=daemon_thread, daemon=True)
# 推荐方式 2:设置属性
# d_thread = threading.Thread(target=daemon_thread)
# d_thread.daemon = True
d_thread.start()
# 主程序执行一些任务
for i in range(5):
print("