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 的基本步骤:

  1. 创建一个 Queue 对象:

    from queue import Queue
    q = Queue()
    
  2. 向队列中添加项目:

    q.put(item)
    
  3. 从队列中获取项目:

    item = q.get()
    
  4. 等待队列非空后再获取项目:

    item = q.get(block=True, timeout=None)  # 可以设置超时时间
    
  5. 任务完成后标记队列中的项目已处理:

    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 的影响

  1. 线程安全:GIL 确保了在任何时刻只有一个线程执行 Python 字节码,这意味着不需要对 Python 对象的访问进行额外的同步,因为不会有多个线程同时修改它们。
  2. 性能限制:由于 GIL 的存在,即使在多核处理器上,Python 的多线程程序也不能有效地利用多核资源来执行 CPU 密集型任务。这是因为 GIL 限制了真正的并行执行,导致多线程程序在 CPU 密集型任务上的性能提升有限。
  3. 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)的,这意味着当主线程结束时,守护线程也会被强制结束,不管它是否完成了任务。

守护线程的特点
  1. 依附于主线程:守护线程不会阻止程序退出。如果只剩下守护线程在运行,程序将自动退出。
  2. 用于后台任务:守护线程通常用于执行一些不会阻塞程序结束的任务,比如日志记录、状态监控等。
  3. 自动结束:当主线程结束时,所有守护线程都会被终止,无论它们是否完成了任务。
守护线程的意义
  1. 后台任务处理: 守护线程通常用于执行一些后台任务,比如日志记录、状态监控、资源清理、垃圾回收等。这些任务不需要用户直接干预,但对系统的稳定运行至关重要。
  2. 防止程序挂起: 守护线程允许程序在完成主要任务后立即退出,而不是无限期地等待后台任务完成。这对于命令行工具或者需要快速启动和退出的应用程序尤其重要。
  3. 资源管理: 在某些情况下,程序可能需要释放资源(如关闭文件描述符、网络连接等),而这些操作可以在守护线程中执行,确保即使主程序结束,这些资源也能被妥善处理。
  4. 提高效率: 对于一些不需要即时反馈的任务,使用守护线程可以提高程序的效率。主线程可以继续处理用户交互或其他重要任务,而将耗时的后台任务交给守护线程处理。
  5. 服务线程: 守护线程可以作为服务线程,为整个应用程序提供服务,比如处理请求队列、监控网络状态等,而不需要阻塞主线程。
  6. 异常处理: 如果主线程因为某些原因崩溃或异常退出,守护线程可以负责最后的清理工作,比如记录异常信息、释放资源等。
  7. 平滑用户体验: 对于图形用户界面(GUI)应用程序,守护线程可以用来处理耗时的任务,避免界面冻结,从而提供更平滑的用户体验。
  8. 调试和诊断: 守护线程可以用来收集和记录程序运行时的信息,这对于调试和诊断问题非常有用。

在创建线程时,或者在启动线程之前,通过设置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列表,等待每个线程完成,确保主程序在所有子线程完成后才继续执行。

更多推荐