Python基础-线程
Python的多线程模块主要是threading,它提供了丰富的接口来创建和管理线程。
线程基本知识点
1. 导入**threading**模块
首先,你需要导入Python的threading模块。
import threading
2. 定义线程任务
你需要定义一个函数,这个函数将作为线程执行的任务。这个函数可以包含任何代码,但通常应该避免执行阻塞操作,如I/O操作,因为这会阻塞整个线程。
def thread_task():
print("Thread is running")
# 这里可以添加更多的代码
3. 创建线程
使用threading.Thread类创建一个线程对象,并将目标函数作为参数传递。
thread = threading.Thread(target=thread_task)
4. 启动线程 start()
使用start()方法启动线程。
start() 方法的一些关键点和用途:
启动线程执行
start()方法会调用线程对象的目标函数(即你在创建Thread对象时通过target参数指定的函数)。- 这个目标函数将在新的线程中异步执行,这意味着主线程不会等待这个函数完成,而是继续执行后续的代码。
线程调度
- 在多线程编程中,操作系统负责调度线程的执行。
start()方法告诉操作系统开始调度这个线程。 - 线程的执行顺序和时间是由操作系统的调度器决定的,而不是由 Python 程序直接控制。
只能调用一次
- 一个线程的
start()方法只能被调用一次。如果一个线程已经启动,再次调用start()方法将会引发RuntimeError。 - 一旦线程执行完毕,它就不能被重新启动。如果你需要重新执行相同的任务,你必须创建一个新的线程对象。
thread.start()
演示了 start() 方法的使用:
import threading
import time
def thread_function(name):
print(f"Thread {name}: starting")
time.sleep(1) # 模拟耗时操作
print(f"Thread {name}: finishing")
# 创建线程对象,指定目标函数和参数
thread = threading.Thread(target=thread_function, args=("Example",))
# 启动线程
thread.start()
print("Main thread: thread has been started")
# 等待线程完成
thread.join()
print("Main thread: thread has finished")
在这个例子中,创建了一个线程对象,指定了它要执行的目标函数 thread_function 和一个参数 "Example"。调用 start() 方法后,线程开始执行,主线程打印一条消息表示线程已启动,然后继续执行。线程执行完毕后,主线程通过调用 join() 方法等待线程结束,最后打印一条消息表示线程已结束。
5. 等待线程完成 join()
使用join()方法等待线程完成。这可以确保主程序在子线程完成之前不会退出。
join() 方法的一些关键点和用途:
阻塞调用线程
- 当你在主线程中对一个子线程调用
join()时,主线程会等待该子线程执行完成之后才继续执行。 - 这意味着
join()方法可以用来同步线程,确保某些线程在主程序继续执行之前已经完成它们的任务。
确保资源正确释放
- 在多线程程序中,如果主线程结束而子线程还在运行,那么程序可能不会正常退出,因为子线程可能还在使用一些资源。
- 通过使用
join()方法,你可以确保在程序退出之前所有子线程都已经完成,这样可以正确地释放资源。
等待线程完成
- 如果你的程序中有多个线程在执行不同的任务,你可能需要等待所有任务都完成后再进行下一步操作。
- 在这种情况下,你可以在每个线程上调用
join()方法,以确保所有线程都执行完毕。
thread.join()
演示了 join() 方法的使用:
import threading
import time
def thread_function(name):
print(f"Thread {name}: starting")
time.sleep(2)
print(f"Thread {name}: finishing")
# 创建线程
thread1 = threading.Thread(target=thread_function, args=("One",))
thread2 = threading.Thread(target=thread_function, args=("Two",))
# 启动线程
thread1.start()
thread2.start()
# 在主线程中等待子线程完成
thread1.join()
thread2.join()
print("Main thread: both threads have finished")
在这个例子中,我们创建了两个线程,每个线程都会打印一条开始消息,然后休眠2秒钟,最后打印一条结束消息。在主线程中,我们调用了每个线程的 join() 方法,这样主线程会等待这两个线程都执行完毕后才打印最后的结束消息。
6. 处理多个线程
如果你需要同时运行多个线程,可以创建多个线程对象,并分别启动它们。
def thread_task_1():
print("Thread 1 is running")
def thread_task_2():
print("Thread 2 is running")
thread1 = threading.Thread(target=thread_task_1)
thread2 = threading.Thread(target=thread_task_2)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
7. 线程同步 Lock
在多线程环境中,可能会遇到竞态条件,即多个线程同时访问共享资源。为了避免这种情况,可以使用锁(Locks)。
Lock 是一个同步原语,可以用来防止多个线程同时执行某段代码,从而避免共享资源的冲突。当一个线程获取(锁定)一个锁时,其他尝试获取同一个锁的线程将被阻塞,直到锁被释放。
创建一个Lock对象:
lock = threading.Lock()
在需要同步的代码块之前获取(锁定)Lock:
lock.acquire()
在代码块执行完毕后释放(解锁)Lock:
lock.release()
使用Lock的上下文管理器
Python 的 Lock 对象提供了一个上下文管理器,使得获取和释放锁更加方便和安全。使用 with 语句可以自动管理锁的获取和释放,即使在发生异常时也能确保锁被释放。
with lock:
# 需要同步的代码块
pass # 这里放置实际的代码
Lock的使用示例
import threading
# 共享资源
shared_resource = 0
# 锁对象
lock = threading.Lock()
def increment():
global shared_resource
with lock: # 使用上下文管理器自动获取和释放锁
local_copy = shared_resource
local_copy += 1
shared_resource = local_copy
# 创建线程
threads = []
for i in range(10):
thread = threading.Thread(target=increment)
threads.append(thread)
thread.start()
# 等待所有线程完成
for thread in threads:
thread.join()
print(f"Shared resource value: {shared_resource}")
在这个例子中,我们创建了一个共享资源 shared_resource 和一个 Lock 对象。每个线程都会尝试修改这个共享资源。通过使用 with lock: 语句,我们确保在同一时刻只有一个线程能够修改 shared_resource,从而避免了数据竞争。
8. 线程安全的数据结构 Queue
threading模块还提供了一些线程安全的数据结构,如Queue,用于线程间的数据传递。
Queue 类在 queue 模块中,提供了一个 FIFO(先进先出)的数据结构,允许多个生产者线程往队列中添加项目,以及多个消费者线程从队列中取出项目。Queue 类内部已经处理了所有必要的锁定,因此是线程安全的。
如何使用 Queue?
以下是使用 Queue 的基本步骤:
-
创建一个
Queue对象:from queue import Queue q = Queue() -
向队列中添加项目:
q.put(item) -
从队列中获取项目:
item = q.get() -
等待队列非空后再获取项目:
item = q.get(block=True, timeout=None) # 可以设置超时时间 -
任务完成后标记队列中的项目已处理:
q.task_done()
Queue 的方法
put(item):将一个项目放入队列。如果队列已满(如果设置了最大大小),此方法会阻塞,直到队列中有空位。get(block=True, timeout=None):从队列中取出一个项目。如果队列为空,且block参数为True,则此方法会阻塞,直到队列中有项目可用。timeout参数可以设置超时时间。task_done():告诉队列一个项目已经被处理。这通常在消费者线程中调用,特别是在使用join()方法等待所有项目都被处理时。join():阻塞,直到队列中的所有项目都被task_done()调用处理。
Queue 的使用示例
下面是一个使用 Queue 的示例,演示了生产者-消费者模型:
from queue import Queue
import threading
import time
def producer(q):
for i in range(5):
print(f"Producing {i}")
q.put(i)
time.sleep(1)
def consumer(q):
while True:
item = q.get()
print(f"Consuming {item}")
if item is None: # 一个特殊的停止信号
break
q.task_done()
# 创建队列
q = Queue()
# 创建生产者线程
producer_thread = threading.Thread(target=producer, args=(q,))
producer_thread.start()
# 创建消费者线程
consumer_thread = threading.Thread(target=consumer, args=(q,))
consumer_thread.start()
# 等待生产者线程完成
producer_thread.join()
# 发送停止信号
q.put(None)
# 等待消费者线程完成
consumer_thread.join()
print("Done")
在这个例子中,生产者线程向队列中添加项目,消费者线程从队列中取出并处理项目。当生产者线程完成生产后,它通过向队列中添加一个特殊的 None 项目来通知消费者线程停止。
9. 理解全局解释器锁(GIL)
Python的全局解释器锁(GIL)是一个机制,它确保在任何时刻只有一个线程执行Python字节码。这意味着在CPython(Python的官方和最常用的实现)中,多线程可能不会带来性能上的提升,特别是在CPU密集型任务中。然而,对于I/O密集型任务,多线程仍然是有用的。
因此,GIL 被引入来保护内存管理,避免多个线程同时执行 Python 字节码,从而避免数据竞争和内存不一致的问题。
GIL 的影响
- 线程安全:GIL 确保了在任何时刻只有一个线程执行 Python 字节码,这意味着不需要对 Python 对象的访问进行额外的同步,因为不会有多个线程同时修改它们。
- 性能限制:由于 GIL 的存在,即使在多核处理器上,Python 的多线程程序也不能有效地利用多核资源来执行 CPU 密集型任务。这是因为 GIL 限制了真正的并行执行,导致多线程程序在 CPU 密集型任务上的性能提升有限。
- I/O 密集型任务的优势:对于 I/O 密集型任务,GIL 的影响较小,因为线程在等待 I/O 操作(如文件读写、网络通信)时会释放 GIL,允许其他线程运行。
GIL 与多线程编程
multithreading 函数尝试通过创建四个线程来并行计算一个列表的总和。然而,由于 GIL 的存在,这些线程并不能真正并行执行。当一个线程执行时,其他线程必须等待。这意味着即使有多个线程,它们也不能同时利用多核 CPU 来加速计算。
import threading
from queue import Queue
import copy
import time
def job(l, q):
res = sum(l)
q.put(res)
def multithreading(l):
q = Queue()
threads = []
for i in range(4):
t = threading.Thread(target=job, args=(copy.copy(l), q), name='T%i' % i)
t.start()
threads.append(t)
[t.join() for t in threads]
total = 0
for _ in range(4):
total += q.get()
print(total)
def normal(l):
total = sum(l)
print(total)
if __name__ == '__main__':
l = list(range(1000000))
s_t = time.time()
normal(l*4)
print('normal: ', time.time()-s_t)
s_t = time.time()
multithreading(l)
print('multithreading: ', time.time()-s_t)
分析代码
normal函数计算列表l的四倍长度的总和,这是一个 CPU 密集型任务。multithreading函数创建四个线程,每个线程计算列表l的总和并将结果放入队列q中。主线程等待所有子线程完成后,从队列中取出结果并计算总和。
结果
- 在单线程的
normal函数中,计算可能相对较慢,因为它在单个线程中执行所有计算。 - 在多线程的
multithreading函数中,由于 GIL 的限制,多线程并不能显著提高性能,尤其是在 CPU 密集型任务中。实际上,由于线程创建和管理的开销,多线程版本可能比单线程版本更慢。
结论
GIL 对 Python 的多线程编程有显著影响,特别是在 CPU 密集型任务中。对于这类任务,可以考虑使用多进程(通过 multiprocessing 模块)来实现真正的并行计算,因为每个进程有自己的 Python 解释器和内存空间,不受 GIL 的限制。对于 I/O 密集型任务,多线程仍然是一个有效的并发编程模型。
线程基本操作
获取当前活动线程 enumerate
你可以使用threading.enumerate()函数来获取一个包含所有当前活动线程的列表。
import threading
# 获取当前所有活动线程
threads = threading.enumerate()
for thread in threads:
print(thread.name)
获取线程名称 name
每个线程都有一个名称,可以通过name属性获取。
my_thread = threading.Thread(target=my_function, name="MyThread")
print(my_thread.name) # 输出: MyThread
获取线程的标识符 ident
每个线程都有一个唯一的标识符,可以通过ident属性获取。
my_thread = threading.Thread(target=my_function)
print(my_thread.ident) # 输出: 线程的唯一标识符(一个长整数)
检查线程是否活着 is_alive
使用is_alive()方法检查线程是否还活着(即是否还在执行)。
my_thread = threading.Thread(target=my_function)
my_thread.start()
print(my_thread.is_alive()) # 输出: True,因为线程正在运行
my_thread.join()
print(my_thread.is_alive()) # 输出: False,因为线程已经完成
设置守护线程 daemon
守护线程(Daemon Thread)是后台线程,用于执行一些后台任务,如垃圾回收、系统日志维护等。在Python中,守护线程的生命周期是依附于主线程(Main Thread)的,这意味着当主线程结束时,守护线程也会被强制结束,不管它是否完成了任务。
守护线程的特点
- 依附于主线程:守护线程不会阻止程序退出。如果只剩下守护线程在运行,程序将自动退出。
- 用于后台任务:守护线程通常用于执行一些不会阻塞程序结束的任务,比如日志记录、状态监控等。
- 自动结束:当主线程结束时,所有守护线程都会被终止,无论它们是否完成了任务。
守护线程的意义
- 后台任务处理: 守护线程通常用于执行一些后台任务,比如日志记录、状态监控、资源清理、垃圾回收等。这些任务不需要用户直接干预,但对系统的稳定运行至关重要。
- 防止程序挂起: 守护线程允许程序在完成主要任务后立即退出,而不是无限期地等待后台任务完成。这对于命令行工具或者需要快速启动和退出的应用程序尤其重要。
- 资源管理: 在某些情况下,程序可能需要释放资源(如关闭文件描述符、网络连接等),而这些操作可以在守护线程中执行,确保即使主程序结束,这些资源也能被妥善处理。
- 提高效率: 对于一些不需要即时反馈的任务,使用守护线程可以提高程序的效率。主线程可以继续处理用户交互或其他重要任务,而将耗时的后台任务交给守护线程处理。
- 服务线程: 守护线程可以作为服务线程,为整个应用程序提供服务,比如处理请求队列、监控网络状态等,而不需要阻塞主线程。
- 异常处理: 如果主线程因为某些原因崩溃或异常退出,守护线程可以负责最后的清理工作,比如记录异常信息、释放资源等。
- 平滑用户体验: 对于图形用户界面(GUI)应用程序,守护线程可以用来处理耗时的任务,避免界面冻结,从而提供更平滑的用户体验。
- 调试和诊断: 守护线程可以用来收集和记录程序运行时的信息,这对于调试和诊断问题非常有用。
在创建线程时,或者在启动线程之前,通过设置daemon属性为True来指定一个线程为守护线程。
import threading
import time
def daemon_task():
while True:
print("Daemon thread is running")
time.sleep(2)
# 创建线程对象
daemon_thread = threading.Thread(target=daemon_task)
# 设置为守护线程
daemon_thread.daemon = True
# 启动线程
daemon_thread.start()
# 主线程执行的代码
print("Main thread is running")
time.sleep(3) # 主线程睡眠3秒后结束
在这个例子中,主线程在睡眠3秒后结束,而守护线程daemon_thread会在主线程结束时被自动终止。
注意事项
- 资源清理:由于守护线程可能会在任何时候被终止,所以不应该在守护线程中执行需要清理资源的操作,比如关闭文件、释放锁等。
- 不适合长时间任务:守护线程不适合执行长时间运行的任务,因为它们可能在任何时候被终止。
- 调试难度:由于守护线程的生命周期不确定,调试守护线程中的问题可能会更加困难。
守护线程与非守护线程
非守护线程(User Thread)是默认的线程类型,它们会一直运行直到结束。如果所有非守护线程都结束了,只剩下守护线程,程序会退出。非守护线程可以用来执行需要确保完成的任务,比如文件下载、数据库操作等。
获取主线程 current_thread
主线程(程序启动时创建的线程)可以通过current_thread()函数获取。
import threading
main_thread = threading.current_thread()
print(main_thread.name) # 输出:MainThread
添加线程到列表
如果你想要管理一个线程列表,可以创建一个列表并在其中添加线程对象。
创建一个空列表来存储线程对象。
threads = []
例子
import threading
import time
def thread_task():
print(f"Thread {threading.current_thread().name} is running")
time.sleep(1) # 模拟耗时操作
# 创建线程列表
threads = []
# 创建并添加线程到列表
for i in range(5): # 创建5个线程
t = threading.Thread(target=thread_task)
t.start() # 启动线程
threads.append(t) # 添加到线程列表
# 等待所有线程完成
for t in threads:
t.join()
print("All threads have completed.")
在这个示例中,我们创建了5个线程,每个线程都会执行thread_task函数。我们首先将每个创建的线程对象添加到threads列表中,然后启动它们。最后,我们遍历threads列表,等待每个线程完成,确保主程序在所有子线程完成后才继续执行。
更多推荐
所有评论(0)