Python 魔法学院 – 第18篇:Python 多线程 ⭐⭐⭐

目录

  • 引言
  • 1. 多线程编程基础
  • 1.1 什么是多线程?
  • 1.2 为什么需要多线程?
  • 1.3 Python 中的多线程模块
  • 2. 创建和启动线程
  • 2.1 使用 `threading.Thread` 创建线程
  • 2.2 使用 `target` 参数创建线程
  • 3. 线程同步
  • 3.1 为什么需要线程同步?
  • 3.2 使用 `Lock` 实现线程同步
  • 3.3 使用 `RLock` 实现可重入锁
  • 4. 线程间通信
  • 4.1 使用 `Queue` 实现线程间通信
  • 4.2 使用 `Condition` 实现线程间通信
  • 5. 线程池
  • 5.1 使用 `ThreadPoolExecutor` 创建线程池
  • 6. 多线程编程的陷阱与注意事项
  • 6.1 GIL(全局解释器锁)
  • 6.2 死锁
  • 6.3 线程安全
  • 7. 总结
  • 引言

    在编程的世界里,时间就是金钱。随着计算机硬件的飞速发展,多核处理器已经成为标配。如何充分利用这些计算资源,提升程序的执行效率,成为了每个开发者必须面对的挑战。Python 作为一门简洁而强大的编程语言,提供了多种并发编程的方式,其中多线程是最常用的一种。

    本文将带你深入 Python 多线程的世界,从基础概念到高级应用,逐步揭开多线程编程的神秘面纱。无论你是初学者、中级开发者,还是资深开发者,都能在这里找到适合自己的知识点。


    1. 多线程编程基础

    1.1 什么是多线程?

    多线程(Multithreading)是指在一个进程中同时运行多个线程,每个线程执行不同的任务。线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。

    详细解释:

  • 进程:进程是操作系统分配资源的基本单位,每个进程都有独立的内存空间和系统资源。比如,你打开一个浏览器和一个音乐播放器,它们就是两个不同的进程。
  • 线程:线程是进程中的一个执行流,多个线程共享进程的内存空间和系统资源。比如,浏览器中的一个标签页可以看作一个线程,它和其他标签页共享浏览器的资源。
  • 多线程:多线程允许在一个进程中同时运行多个线程,每个线程可以执行不同的任务。比如,一个下载软件可以同时下载多个文件,每个下载任务由一个线程负责。
  • 比喻:

  • 将进程比作一个工厂,线程比作工厂中的工人。工厂(进程)提供了资源和环境,工人(线程)在工厂中完成具体的工作。

  • 1.2 为什么需要多线程?

  • 提高响应速度:在 GUI 应用程序中,使用多线程可以避免界面卡顿。
  • 充分利用多核 CPU:多线程可以并行执行任务,充分利用多核 CPU 的计算能力。
  • 简化编程模型:多线程可以将复杂的任务分解为多个简单的任务,简化编程模型。
  • 详细解释:

  • 提高响应速度:在 GUI 应用程序中,主线程负责处理用户界面事件,如果主线程执行耗时操作,界面会卡顿。通过多线程,可以将耗时操作放到后台线程中执行,保持界面的响应速度。比如,在一个视频播放器中,主线程负责显示界面,另一个线程负责解码视频。
  • 充分利用多核 CPU:现代 CPU 通常有多个核心,多线程可以将任务分配到不同的核心上并行执行,提高程序的执行效率。比如,一个数据处理程序可以将数据分成多份,每个线程处理一份数据。
  • 简化编程模型:多线程可以将复杂的任务分解为多个简单的任务,每个线程负责一个任务,简化编程模型。比如,一个网络爬虫可以使用多个线程同时爬取多个网页。

  • 1.3 Python 中的多线程模块

    Python 提供了 threading 模块来支持多线程编程。threading 模块是 Python 标准库的一部分,使用起来非常方便。

    详细解释:

  • threading 模块提供了 Thread 类来创建和管理线程。
  • threading 模块还提供了多种同步机制,如 LockRLockCondition 等,用于线程间的同步和通信。
  • 代码示例:

    import threading
    
    def worker():
        print("线程开始执行")
        for i in range(5):
            print(f"线程执行中: {i}")
        print("线程执行结束")
    
    # 创建线程实例
    thread = threading.Thread(target=worker)
    
    # 启动线程
    thread.start()
    
    # 等待线程执行完毕
    thread.join()
    
    print("主线程结束")
    

    结果为:

    线程开始执行
    线程执行中: 0
    线程执行中: 1
    线程执行中: 2
    线程执行中: 3
    线程执行中: 4
    线程执行结束
    主线程结束
    

    解释:

  • threading.Threadtarget 参数指定线程要执行的函数。
  • thread.start() 启动线程,线程开始执行 worker 函数。
  • thread.join() 等待线程执行完毕,主线程才会继续执行。

  • 2. 创建和启动线程

    2.1 使用 threading.Thread 创建线程

    在 Python 中,创建线程的最简单方式是使用 threading.Thread 类。我们可以通过继承 threading.Thread 类并重写 run 方法来定义线程的执行逻辑。

    代码示例:

    import threading
    
    class MyThread(threading.Thread):
        def run(self):
            print("线程开始执行")
            for i in range(5):
                print(f"线程执行中: {i}")
            print("线程执行结束")
    
    # 创建线程实例
    thread = MyThread()
    
    # 启动线程
    thread.start()
    
    # 等待线程执行完毕
    thread.join()
    
    print("主线程结束")
    

    结果为:

    线程开始执行
    线程执行中: 0
    线程执行中: 1
    线程执行中: 2
    线程执行中: 3
    线程执行中: 4
    线程执行结束
    主线程结束
    

    解释:

  • MyThread 类继承自 threading.Thread,并重写了 run 方法,run 方法定义了线程的执行逻辑。
  • thread.start() 启动线程,线程开始执行 run 方法。
  • thread.join() 等待线程执行完毕,主线程才会继续执行。

  • 2.2 使用 target 参数创建线程

    除了继承 threading.Thread 类,我们还可以通过传递 target 参数来创建线程。target 参数指定线程要执行的函数。

    代码示例:

    import threading
    
    def worker():
        print("线程开始执行")
        for i in range(5):
            print(f"线程执行中: {i}")
        print("线程执行结束")
    
    # 创建线程实例
    thread = threading.Thread(target=worker)
    
    # 启动线程
    thread.start()
    
    # 等待线程执行完毕
    thread.join()
    
    print("主线程结束")
    

    结果为:

    线程开始执行
    线程执行中: 0
    线程执行中: 1
    线程执行中: 2
    线程执行中: 3
    线程执行中: 4
    线程执行结束
    主线程结束
    

    解释:

  • threading.Threadtarget 参数指定线程要执行的函数。
  • thread.start() 启动线程,线程开始执行 worker 函数。
  • thread.join() 等待线程执行完毕,主线程才会继续执行。

  • 3. 线程同步

    3.1 为什么需要线程同步?

    在多线程环境中,多个线程可能会同时访问共享资源,导致数据不一致或程序行为异常。为了避免这种情况,我们需要使用线程同步机制。

    详细解释:

  • 共享资源:多个线程共享的内存或资源。
  • 竞争条件:多个线程同时访问共享资源,导致数据不一致或程序行为异常。
  • 线程同步:通过同步机制确保同一时间只有一个线程访问共享资源。
  • 比喻:

  • 将共享资源比作一个厕所,线程比作人。如果没有锁,多个人可能同时进入厕所,导致混乱。通过锁机制,确保一次只有一个人可以使用厕所。

  • 3.2 使用 Lock 实现线程同步

    Lock 是 Python 中最基本的线程同步机制。它允许我们确保同一时间只有一个线程可以访问共享资源。

    代码示例:

    import threading
    
    # 共享资源
    counter = 0
    
    # 创建锁
    lock = threading.Lock()
    
    def increment():
        global counter
        for _ in range(100000):
            # 获取锁
            lock.acquire()
            counter += 1
            # 释放锁
            lock.release()
    
    # 创建多个线程
    threads = []
    for i in range(10):
        thread = threading.Thread(target=increment)
        threads.append(thread)
        thread.start()
    
    # 等待所有线程执行完毕
    for thread in threads:
        thread.join()
    
    print(f"最终结果: {counter}")
    

    结果为:

    最终结果: 1000000
    

    解释:

  • lock.acquire() 获取锁,确保同一时间只有一个线程可以执行 counter += 1
  • lock.release() 释放锁,允许其他线程获取锁。
  • 通过锁机制,确保 counter 的最终结果为 1000000

  • 3.3 使用 RLock 实现可重入锁

    RLock(可重入锁)允许同一个线程多次获取锁,而不会导致死锁。这在递归函数中非常有用。

    代码示例:

    import threading
    
    # 共享资源
    counter = 0
    
    # 创建可重入锁
    rlock = threading.RLock()
    
    def increment():
        global counter
        for _ in range(100000):
            # 获取锁
            rlock.acquire()
            counter += 1
            # 释放锁
            rlock.release()
    
    # 创建多个线程
    threads = []
    for i in range(10):
        thread = threading.Thread(target=increment)
        threads.append(thread)
        thread.start()
    
    # 等待所有线程执行完毕
    for thread in threads:
        thread.join()
    
    print(f"最终结果: {counter}")
    

    结果为:

    最终结果: 1000000
    

    解释:

  • RLock 允许同一个线程多次获取锁,而不会导致死锁。
  • 在递归函数中,RLock 非常有用,因为它允许同一个线程多次获取锁。

  • 4. 线程间通信

    4.1 使用 Queue 实现线程间通信

    Queue 是 Python 中用于线程间通信的常用数据结构。它是线程安全的,可以在多个线程之间安全地传递数据。

    代码示例:

    import threading
    import queue
    import time
    
    # 创建队列
    q = queue.Queue()
    
    def producer():
        for i in range(5):
            print(f"生产者生产: {i}")
            q.put(i)
            time.sleep(1)
    
    def consumer():
        while True:
            item = q.get()
            if item is None:
                break
            print(f"消费者消费: {item}")
            q.task_done()
    
    # 创建生产者线程
    producer_thread = threading.Thread(target=producer)
    
    # 创建消费者线程
    consumer_thread = threading.Thread(target=consumer)
    
    # 启动线程
    producer_thread.start()
    consumer_thread.start()
    
    # 等待生产者线程执行完毕
    producer_thread.join()
    
    # 等待队列中的所有任务完成
    q.join()
    
    # 发送结束信号
    q.put(None)
    
    # 等待消费者线程执行完毕
    consumer_thread.join()
    
    print("主线程结束")
    

    结果为:

    生产者生产: 0
    消费者消费: 0
    生产者生产: 1
    消费者消费: 1
    生产者生产: 2
    消费者消费: 2
    生产者生产: 3
    消费者消费: 3
    生产者生产: 4
    消费者消费: 4
    主线程结束
    

    解释:

  • Queue 是线程安全的队列,可以在多个线程之间安全地传递数据。
  • q.put(item) 将数据放入队列。
  • q.get() 从队列中获取数据。
  • q.task_done() 标记任务完成。
  • q.join() 等待队列中的所有任务完成。

  • 4.2 使用 Condition 实现线程间通信

    Condition 是 Python 中用于线程间通信的高级同步机制。它允许线程等待特定条件满足后再继续执行。

    代码示例:

    import threading
    
    # 创建条件变量
    condition = threading.Condition()
    
    # 共享资源
    items = []
    
    def producer():
        with condition:
            print("生产者开始生产")
            items.append("item")
            print("生产者生产了一个 item")
            condition.notify()  # 通知消费者
    
    def consumer():
        with condition:
            while not items:
                print("消费者等待生产")
                condition.wait()  # 等待生产者通知
            print(f"消费者消费了: {items.pop()}")
    
    # 创建生产者线程
    producer_thread = threading.Thread(target=producer)
    
    # 创建消费者线程
    consumer_thread = threading.Thread(target=consumer)
    
    # 启动线程
    consumer_thread.start()
    producer_thread.start()
    
    # 等待线程执行完毕
    producer_thread.join()
    consumer_thread.join()
    
    print("主线程结束")
    

    结果为:

    消费者等待生产
    生产者开始生产
    生产者生产了一个 item
    消费者消费了: item
    主线程结束
    

    解释:

  • Condition 允许线程等待特定条件满足后再继续执行。
  • condition.wait() 使线程等待,直到其他线程调用 condition.notify()condition.notify_all()
  • condition.notify() 唤醒一个等待的线程。
  • condition.notify_all() 唤醒所有等待的线程。

  • 5. 线程池

    5.1 使用 ThreadPoolExecutor 创建线程池

    ThreadPoolExecutor 是 Python 中用于管理线程池的高级工具。它可以自动管理线程的创建和销毁,简化多线程编程。

    代码示例:

    from concurrent.futures import ThreadPoolExecutor
    import time
    
    def task(n):
        print(f"任务 {n} 开始执行")
        time.sleep(2)
        print(f"任务 {n} 执行结束")
        return n * n
    
    # 创建线程池
    with ThreadPoolExecutor(max_workers=3) as executor:
        # 提交任务
        futures = [executor.submit(task, i) for i in range(5)]
    
        # 获取任务结果
        for future in futures:
            print(f"任务结果: {future.result()}")
    
    print("主线程结束")
    

    结果为:

    任务 0 开始执行
    任务 1 开始执行
    任务 2 开始执行
    任务 0 执行结束
    任务 3 开始执行
    任务 1 执行结束
    任务 4 开始执行
    任务 2 执行结束
    任务 3 执行结束
    任务 4 执行结束
    任务结果: 0
    任务结果: 1
    任务结果: 4
    任务结果: 9
    任务结果: 16
    主线程结束
    

    解释:

  • ThreadPoolExecutor 自动管理线程的创建和销毁。
  • executor.submit(task, i) 提交任务到线程池。
  • future.result() 获取任务的结果。

  • 6. 多线程编程的陷阱与注意事项

    6.1 GIL(全局解释器锁)

    Python 的全局解释器锁(GIL)是一个互斥锁,它确保同一时间只有一个线程执行 Python 字节码。这意味着在多线程程序中,即使有多个 CPU 核心,Python 也无法真正实现并行执行。

    详细解释:

  • GIL:全局解释器锁,确保同一时间只有一个线程执行 Python 字节码。
  • 影响:GIL 限制了 Python 多线程程序的并行执行能力,特别是在 CPU 密集型任务中。
  • 代码示例:

    import threading
    
    def cpu_bound_task():
        result = 0
        for _ in range(10**7):
            result += 1
        print(f"任务完成: {result}")
    
    # 创建多个线程
    threads = []
    for i in range(4):
        thread = threading.Thread(target=cpu_bound_task)
        threads.append(thread)
        thread.start()
    
    # 等待所有线程执行完毕
    for thread in threads:
        thread.join()
    
    print("主线程结束")
    

    结果为:

    任务完成: 10000000
    任务完成: 10000000
    任务完成: 10000000
    任务完成: 10000000
    主线程结束
    

    解释:

  • 由于 GIL 的存在,即使有多个线程,Python 也无法真正实现并行执行 CPU 密集型任务。
  • 如果需要充分利用多核 CPU,可以考虑使用多进程(multiprocessing 模块)。

  • 6.2 死锁

    死锁是指两个或多个线程互相等待对方释放锁,导致程序无法继续执行。为了避免死锁,我们应该尽量避免嵌套锁,并确保锁的获取和释放顺序一致。

    详细解释:

  • 死锁:两个或多个线程互相等待对方释放锁,导致程序无法继续执行。
  • 避免死锁:尽量避免嵌套锁,确保锁的获取和释放顺序一致。
  • 代码示例:

    import threading
    
    # 创建两个锁
    lock1 = threading.Lock()
    lock2 = threading.Lock()
    
    def thread1():
        with lock1:
            print("线程1获取了锁1")
            with lock2:
                print("线程1获取了锁2")
    
    def thread2():
        with lock2:
            print("线程2获取了锁2")
            with lock1:
                print("线程2获取了锁1")
    
    # 创建两个线程
    t1 = threading.Thread(target=thread1)
    t2 = threading.Thread(target=thread2)
    
    # 启动线程
    t1.start()
    t2.start()
    
    # 等待线程执行完毕
    t1.join()
    t2.join()
    
    print("主线程结束")
    

    结果为:

    线程1获取了锁1
    线程2获取了锁2
    (程序卡住,无法继续执行)
    

    解释:

  • thread1 获取了 lock1,然后尝试获取 lock2
  • thread2 获取了 lock2,然后尝试获取 lock1
  • 两个线程互相等待对方释放锁,导致死锁。
  • 解决方案:

  • 确保所有线程以相同的顺序获取锁。

  • 6.3 线程安全

    线程安全是指多个线程同时访问共享资源时,程序的行为是正确的。为了确保线程安全,我们应该使用线程同步机制,如 LockRLockCondition 等。

    详细解释:

  • 线程安全:多个线程同时访问共享资源时,程序的行为是正确的。
  • 确保线程安全:使用线程同步机制,如 LockRLockCondition 等。
  • 代码示例:

    import threading
    
    # 共享资源
    counter = 0
    
    # 创建锁
    lock = threading.Lock()
    
    def increment():
        global counter
        for _ in range(100000):
            # 获取锁
            lock.acquire()
            counter += 1
            # 释放锁
            lock.release()
    
    # 创建多个线程
    threads = []
    for i in range(10):
        thread = threading.Thread(target=increment)
        threads.append(thread)
        thread.start()
    
    # 等待所有线程执行完毕
    for thread in threads:
        thread.join()
    
    print(f"最终结果: {counter}")
    

    结果为:

    最终结果: 1000000
    

    解释:

  • 通过锁机制,确保多个线程安全地访问共享资源 counter
  • 如果不使用锁,counter 的最终结果可能会小于 1000000

  • 7. 总结

    多线程编程是 Python 并发编程的重要组成部分。通过本文的学习,你应该已经掌握了 Python 多线程编程的基础知识,包括线程的创建与启动、线程同步、线程间通信、线程池等内容。同时,我们也探讨了多线程编程中的一些陷阱与注意事项。


    希望本文能够激发你对 Python 多线程编程的兴趣,并帮助你在实际开发中更好地利用多线程技术。如果你有任何问题或建议,欢迎在评论区留言讨论。

    作者:码力全開

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python 魔法学院 – 第18篇:Python 多线程 ⭐⭐⭐

    发表回复