Python 线程编程:从入门到实战

1. 线程是什么?

在计算机科学中,线程(Thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和系统资源,但拥有各自独立的执行栈和程序计数器。

简单来说,你可以把进程想象成一个工厂,而线程就是工厂里的工人。多个工人(线程)可以在同一个工厂(进程)里同时工作,共享工厂的原料(内存)和工具(资源),从而高效地完成任务。

2. 为什么需要线程?

使用线程主要为了解决以下问题:

  1. 提高程序响应能力:在图形界面(GUI)程序中,主线程负责处理用户交互(如点击按钮),而耗时的操作(如下载文件、复杂计算)可以交给后台线程执行,避免界面"卡死"。
  2. 充分利用多核 CPU:现代 CPU 通常有多个核心,多线程程序可以将任务分配到不同核心上并行执行,显著提升计算密集型任务的性能。
  3. 简化模型:对于需要同时处理多个连接的服务端程序(如 Web 服务器),为每个连接创建一个线程,比使用复杂的异步 I/O 模型更直观易懂。

3. Python 中的线程模块:threading

Python 标准库提供了 threading 模块来创建和管理线程。它是基于更低级的 _thread 模块构建的,提供了更高级、更易用的接口。

3.1 创建线程的两种方式

方式一:继承 Thread

通过继承 threading.Thread 类并重写 run() 方法来定义线程执行体。

python 复制代码
import threading
import time

class MyThread(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        print(f"线程 {self.name} 开始运行")
        time.sleep(2)  # 模拟耗时操作
        print(f"线程 {self.name} 结束运行")

# 创建并启动线程
t1 = MyThread("Thread-1")
t2 = MyThread("Thread-2")

t1.start()
t2.start()

# 等待线程结束
t1.join()
t2.join()

print("主线程结束")
方式二:传入目标函数

将可调用对象(函数)传递给 Thread 构造器。

python 复制代码
import threading
import time

def worker(name, delay):
    print(f"线程 {name} 开始工作,休眠 {delay} 秒")
    time.sleep(delay)
    print(f"线程 {name} 工作完成")

# 创建线程对象,target 指定要运行的函数,args 指定函数的参数
t1 = threading.Thread(target=worker, args=("A", 3))
t2 = threading.Thread(target=worker, args=("B", 1))

t1.start()
t2.start()

t1.join()
t2.join()

print("所有工作线程执行完毕")

3.2 线程的生命周期与常用方法

  • start() : 启动线程,使其进入就绪状态。每个线程只能调用一次 start()
  • run() : 线程的入口点方法。通常不需要直接调用,由 start() 内部调用。
  • join([timeout]) : 阻塞调用线程(通常是主线程),直到被调用 join() 的线程终止。可设置超时时间。
  • is_alive(): 返回线程是否还在运行。
  • name: 线程的名称,可用于调试。
  • daemon : 守护线程属性。如果设置为 True,当主线程退出时,该线程会被强制结束,而不管它是否执行完毕。

4. 线程同步与通信

当多个线程访问共享资源(如变量、文件、数据库连接)时,可能会发生竞态条件(Race Condition) ,导致数据不一致。threading 模块提供了多种同步原语来解决这个问题。

4.1 锁(Lock)

锁是最基本的同步机制,一次只允许一个线程进入"临界区"。

python 复制代码
import threading

# 共享资源
counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        lock.acquire()  # 获取锁
        try:
            counter += 1
        finally:
            lock.release()  # 释放锁

# 创建多个线程同时增加计数器
threads = []
for i in range(10):
    t = threading.Thread(target=increment)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(f"最终计数器值: {counter}")  # 应该是 1000000

使用 with 语句简化锁操作(推荐):

python 复制代码
def increment_with():
    global counter
    for _ in range(100000):
        with lock:  # 自动获取和释放锁
            counter += 1

4.2 信号量(Semaphore)

信号量控制同时访问特定资源的线程数量。

python 复制代码
import threading
import time

# 模拟一个只有3个座位的资源池
semaphore = threading.Semaphore(3)

def access_resource(thread_id):
    with semaphore:
        print(f"线程 {thread_id} 获得了资源访问权")
        time.sleep(2)  # 模拟使用资源
        print(f"线程 {thread_id} 释放了资源")

threads = []
for i in range(10):
    t = threading.Thread(target=access_resource, args=(i,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

4.3 条件变量(Condition)

条件变量用于复杂的线程间协调,允许一个线程等待特定条件成立,并由其他线程通知它。

python 复制代码
import threading
import time
import random

# 生产者-消费者模型
queue = []
MAX_ITEMS = 5
condition = threading.Condition()

def producer():
    for i in range(10):
        time.sleep(random.random())
        with condition:
            while len(queue) == MAX_ITEMS:
                print("队列已满,生产者等待...")
                condition.wait()  # 释放锁并等待
            item = f"产品-{i}"
            queue.append(item)
            print(f"生产者生产了: {item}")
            condition.notify()  # 通知等待的消费者

def consumer():
    for _ in range(10):
        time.sleep(random.random() * 2)
        with condition:
            while len(queue) == 0:
                print("队列为空,消费者等待...")
                condition.wait()  # 释放锁并等待
            item = queue.pop(0)
            print(f"消费者消费了: {item}")
            condition.notify()  # 通知等待的生产者

# 启动生产者和消费者线程
p = threading.Thread(target=producer)
c = threading.Thread(target=consumer)

p.start()
c.start()
p.join()
c.join()

5. 全局解释器锁(GIL)与多线程的局限性

Python(特指 CPython 解释器)有一个著名的特性------全局解释器锁(Global Interpreter Lock, GIL)。GIL 确保同一时刻只有一个线程在执行 Python 字节码。

5.1 GIL 的影响

  • I/O 密集型任务:线程在等待 I/O(如网络请求、文件读写)时会释放 GIL,因此多线程能有效提升 I/O 密集型程序的性能。
  • CPU 密集型任务:由于 GIL 的存在,多线程无法利用多核 CPU 实现真正的并行计算。对于纯计算任务,多线程可能比单线程更慢(因为线程切换有开销)。

5.2 如何绕过 GIL?

  1. 使用多进程(multiprocessing 模块):每个进程有独立的 Python 解释器和内存空间,因此没有 GIL 的限制,能真正利用多核 CPU。
  2. 使用 C 扩展:在 C 扩展中释放 GIL,执行计算密集型操作。
  3. 使用其他 Python 实现:如 Jython(基于 JVM)或 IronPython(基于 .NET),它们没有 GIL。
  4. 使用异步编程(asyncio:对于高并发的 I/O 密集型任务,异步模型比多线程更高效、资源占用更少。

6. 实战:多线程下载器

下面是一个简单的多线程文件下载器示例,演示了线程池的基本用法。

python 复制代码
import threading
import requests
from queue import Queue
import os

class Downloader:
    def __init__(self, num_workers=4):
        self.num_workers = num_workers
        self.queue = Queue()
        self.lock = threading.Lock()

    def download_file(self, url, save_path):
        """下载单个文件"""
        try:
            response = requests.get(url, stream=True, timeout=30)
            response.raise_for_status()
            with open(save_path, 'wb') as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk:
                        f.write(chunk)
            with self.lock:
                print(f"下载成功: {save_path}")
            return True
        except Exception as e:
            with self.lock:
                print(f"下载失败 {url}: {e}")
            return False

    def worker(self):
        """工作线程函数"""
        while True:
            item = self.queue.get()
            if item is None:  # 终止信号
                self.queue.task_done()
                break
            url, save_path = item
            self.download_file(url, save_path)
            self.queue.task_done()

    def add_task(self, url, save_path):
        """添加下载任务"""
        self.queue.put((url, save_path))

    def run(self):
        """启动下载器"""
        threads = []
        for _ in range(self.num_workers):
            t = threading.Thread(target=self.worker)
            t.start()
            threads.append(t)

        # 等待所有任务完成
        self.queue.join()

        # 发送终止信号给工作线程
        for _ in range(self.num_workers):
            self.queue.put(None)
        for t in threads:
            t.join()
        print("所有下载任务完成!")

# 使用示例
if __name__ == "__main__":
    downloader = Downloader(num_workers=3)

    # 添加下载任务(示例URL)
    tasks = [
        ("https://example.com/file1.zip", "file1.zip"),
        ("https://example.com/file2.pdf", "file2.pdf"),
        ("https://example.com/file3.jpg", "file3.jpg"),
    ]

    for url, path in tasks:
        downloader.add_task(url, path)

    downloader.run()

7. 总结与最佳实践

  1. 明确任务类型

    • I/O 密集型(网络、磁盘):多线程通常有效。
    • CPU 密集型 (计算、数据处理):考虑多进程或 concurrent.futures 模块。
  2. 优先使用高层抽象 :对于简单的并行任务,优先使用 concurrent.futures.ThreadPoolExecutor,它提供了更简洁的接口。

  3. 避免共享状态 :尽量设计无共享数据的线程,通过队列(queue.Queue)传递数据,这是最安全的并发模型。

  4. 必须共享时,做好同步 :使用锁、信号量等同步原语保护共享资源,并优先使用 with 语句确保锁被正确释放。

  5. 小心死锁:避免多个线程互相等待对方释放锁的情况。按固定顺序获取锁,或使用带超时的锁。

  6. 善用守护线程:对于后台支持性任务(如日志、心跳),可设置为守护线程,这样主程序退出时它们会自动结束。

Python 线程是处理并发任务的强大工具,理解其原理、局限性和同步机制,是编写健壮、高效并发程序的关键。

相关推荐
l1258651 小时前
# RAG上线评估指标体系:六大核心指标与压测实战全解析
数据库·人工智能·python·mysql·langchain·milvus
我命由我123451 小时前
人脸识别 - 人脸识别选帧
java·人工智能·python·算法·安全·java-ee·人脸识别
SomeB1oody2 小时前
【RustyML入门】7.2. 深入模型持久化
开发语言·后端·机器学习·rust·教程
yaoxin5211232 小时前
502. Java 反射 - 编写 MessageInterceptor 类
java·开发语言
五月底_2 小时前
LIS算法
python·算法·最长子序列
wuyk5552 小时前
Python零基础入门第五章:元组Tuple(不可变容器详解、列表与元组区别)
开发语言·python
三8442 小时前
PHP 污点分析绕过实战:CURL、get_meta_tags 与 fpm_get_status 引入非预定义污点源
开发语言·php
zhanghaha13142 小时前
Python进阶教程:13_math 模块 —— 新手完全指南
数据库·python·机器学习
2601_965798472 小时前
Build a Fast, High-Ranking Restaurant Website with Rolanda Theme
开发语言·ios·swift