Skip to main content

Python 进程与线程

Python 多线程

Python 线程是通过 threading 模块实现的,用于在程序中执行并发任务,适合处理 I/O 密集型操作。 由于全局解释器锁(GIL)的限制,Python 线程无法充分利用多核 CPU 进行并行计算。 线程常用于需要快速响应用户交互或异步处理的任务,如网络请求或文件操作。

1. 线程与进程的关系

在学习多线程之前,我们需要先理解线程和进程的基本概念及其关系。

1.1 定义

  • 进程(Process):是操作系统分配资源(如CPU、内存)的最小单位。每个进程拥有独立的内存空间,进程之间通常是相互隔离的。
  • 线程(Thread):是进程内部的一个执行单元。一个进程可以包含多个线程,线程共享进程的内存空间和资源。

1.2 关系

  • 一个进程至少包含一个线程,通常称为主线程。
  • 多线程共享进程的内存和资源,因此线程间的通信和数据共享比进程间更简单高效。
  • 线程的创建和销毁开销比进程小,因为线程不需要分配独立的内存空间。

1.3 Python中的体现

  • Python使用threading模块来创建和管理线程。
  • Python使用multiprocessing模块来创建和管理进程。
  • 由于Python的全局解释器锁(GIL),多线程在CPU密集型任务中无法充分利用多核CPU,但在I/O密集型任务中表现良好。

2. 多线程的基本使用

让我们从最简单的线程创建开始,逐步掌握Python中线程的基本操作。

Python的threading模块提供了Thread类来创建线程。可以通过指定target参数来定义线程要执行的任务。

以下是一个完整的示例:

import threading
import time

def worker(arg):
    print(f"线程开始运行, arg:{arg}")
    time.sleep(2)  # 模拟耗时任务
    print("线程运行结束")

# 创建线程
thread = threading.Thread(target=worker, args=(1,))
# 启动线程
thread.start()
# 等待线程结束
thread.join()

print("主线程结束")

运行结果:

线程开始运行, arg:1
线程运行结束
主线程结束
  • start():启动线程,开始执行target指定的函数。
  • join():等待线程执行完成,主线程才会继续运行。
  • 每个线程独立运行,但主线程可以通过join()控制执行顺序。

3. 线程池

当需要执行大量短期任务时,频繁创建和销毁线程会带来性能开销。线程池通过复用线程解决了这个问题。

  • 线程池预先创建一组线程,任务提交时从池中分配线程执行,任务完成后线程返回池中待命。
  • 适用于处理多个并行任务,如批量文件处理或网络请求。

Python的concurrent.futures模块提供了ThreadPoolExecutor类来实现线程池。

以下是一个完整示例:

from concurrent.futures import ThreadPoolExecutor
import time

def task(name):
    print(f"任务 {name} 开始")
    time.sleep(2)  # 模拟耗时任务
    print(f"任务 {name} 结束")

# 创建线程池,最多3个线程
with ThreadPoolExecutor(max_workers=3) as executor:
    # 提交任务
    executor.submit(task, "A")
    executor.submit(task, "B")
    executor.submit(task, "C")

print("所有任务已提交")

运行结果:

任务 A 开始
任务 B 开始
任务 C 开始
(等待2秒)
任务 A 结束
任务 B 结束
任务 C 结束
所有任务已提交
  • max_workers:指定线程池中线程的最大数量。
  • submit():将任务提交给线程池,返回一个Future对象(可用于获取任务结果,此处未展示)。

4. 锁与线程安全

多线程共享资源时,如果不加以保护,可能导致数据不一致。锁是确保线程安全的关键工具。

当多个线程同时修改共享资源(如全局变量)时,可能出现竞争条件(Race Condition),导致结果不可预测。

  • threading.Lock:互斥锁,确保同一时间只有一个线程访问共享资源。
  • threading.RLock:可重入锁,允许同一线程多次获取锁。
  • threading.Semaphore:信号量,控制多个线程的访问数量。

以下是一个使用锁的完整示例:

import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    with lock:  # 获取锁,保护共享资源
        counter += 1

# 创建10个线程
threads = []
for _ in range(10):
    thread = threading.Thread(target=increment)
    threads.append(thread)
    thread.start()

# 等待所有线程结束
for thread in threads:
    thread.join()

print(f"最终计数器值: {counter}")

运行结果:

最终计数器值: 10
  • with lock:自动获取和释放锁,防止忘记释放导致死锁。
  • 未加锁时,counter可能小于10,因为多个线程同时操作会覆盖彼此的修改。

5. 同时使用多进程和多线程

在某些复杂场景下,可能需要结合多进程和多线程来充分利用系统资源。

5.1 适用场景

  • CPU密集型任务:使用多进程绕过GIL限制,利用多核CPU。
  • I/O密集型任务:在每个进程内使用多线程提高效率。

5.2 注意事项

  • GIL限制:Python的多线程受GIL限制,CPU密集型任务应优先使用多进程。
  • 资源管理:多进程和多线程混合使用时需注意内存和锁的管理。
  • 和多进程一样,涉及到数据共享的时候,也需要锁来保证数据的一致性。

以下是一个完整示例:

from multiprocessing import Process
import threading
import time

def thread_task(name):
    print(f"线程 {name} 开始")
    time.sleep(2)  # 模拟耗时任务
    print(f"线程 {name} 结束")

def process_task():
    print("进程开始")
    threads = []
    for i in range(3):
        thread = threading.Thread(target=thread_task, args=(i,))
        threads.append(thread)
        thread.start()
    for thread in threads:
        thread.join()
    print("进程结束")

if __name__ == "__main__":
    processes = []
    for _ in range(2):
        process = Process(target=process_task)
        processes.append(process)
        process.start()
    for process in processes:
        process.join()

    print("所有进程结束")

运行结果(可能因线程和进程调度顺序而略有不同):

进程开始
线程 0 开始
线程 1 开始
线程 2 开始
进程开始
线程 0 开始
线程 1 开始
线程 2 开始
(等待2秒)
线程 0 结束
线程 1 结束
线程 2 结束
进程结束
线程 0 结束
线程 1 结束
线程 2 结束
进程结束
所有进程结束