轻松学Python并发编程:线程、进程与异步

在这里插入图片描述


文章目录

  • 引言
  • Python并发编程的主要方法
    • 多线程(Threading)
      • 创建线程的两种方式
    • 多进程(Multiprocessing)
      • 多线程和多进程区别
      • 使用多进程
    • 异步编程(Asyncio)
      • 同步 vs 异步 vs 多线程
      • 异步编程asyncio
      • 常用的asyncio功能
  • 更多操作
    • 使用Queue进行进程间通信
      • Queue模块的基本操作
      • Queue在多线程中的应用(实现生产者-消费者模式)
    • 线程安全锁LLock
      • 锁的基本概念
    • 线程池
      • 线程池ThreadPoolExecutor
      • 线程池的基本操作
      • 演示

引言

并发编程是一种让程序可以同时执行多个任务的编程技术。在计算机中,这意味着多个任务在同一时间段内交替进行,但不一定同时进行。并发编程可以通过多线程、多进程和异步编程来实现。

Python对并发编程的支持

  • 多线程:threading,利用CPU和IO可以同时执行的原理,让CPU不会干巴巴等待IO完成。
  • 多进程:multiprocessing,利用多核CPU的能力,真正的并行执行任务
  • 异步IO:asyncio,在单线程利用CPU和IO同时执行的原理,实现函数异步执行。

Python并发编程的主要方法

Python提供了多种并发编程的方式,包括多线程、多进程和异步编程。每种方式有其适用的场景和优缺点。

多线程(Threading)

多线程是一种并发编程技术,它允许在同一进程中并发执行多个线程。线程是轻量级的子进程,共享同一进程的内存空间和资源,但它们有独立的执行路径和调用栈。通过多线程,程序可以同时处理多个任务,从而提高执行效率。

多线程主要用于以下场景:

  • I/O密集型任务:如文件读写、网络请求、数据库操作等,使用多线程可以有效减少等待时间,提高程序的响应速度。
  • 需要并发执行的任务:如在GUI应用程序中,通过多线程可以确保界面保持响应,同时执行后台任务。
  • 任务之间相互独立:当多个任务相互独立时,可以通过多线程实现并发执行,提高处理效率。

创建线程的两种方式

Python提供了threading模块,用于创建和管理线程。

  1. 继承threading.Thread类:可以通过创建一个子类继承threading.Thread类,并重写run()方法,定义线程要执行的任务。

    import threading
    
    class MyThread(threading.Thread):
        def run(self):
            for i in range(5):
                print(f"Thread {self.name} is running, iteration {i}")
    
    # 创建线程实例
    thread1 = MyThread()
    thread2 = MyThread()
    
    # 启动线程
    thread1.start()
    thread2.start()
    
    # 等待线程完成
    thread1.join()
    thread2.join()
    
  2. 直接使用threading.Thread类:另一种方式是直接创建threading.Thread实例,并传入目标函数和参数。

    import threading
    
    def task(name):
        for i in range(5):
            print(f"Thread {name} is running, iteration {i}")
    
    # 创建并启动线程
    thread1 = threading.Thread(target=task, args=("A",))
    thread2 = threading.Thread(target=task, args=("B",))
    
    thread1.start()
    thread2.start()
    
    thread1.join()
    thread2.join()
    

多进程(Multiprocessing)

多进程是一种并发编程技术,它允许程序在多个进程中并发执行任务。每个进程都有独立的内存空间和资源,由操作系统管理。这使得多进程特别适合用于CPU密集型任务,能够充分利用多核CPU的性能。

多线程和多进程区别

多线程与多进程的主要区别在于:

  • 多线程:线程共享进程的内存空间和资源,适合I/O密集型任务,但受限于Python的全局解释器锁(GIL)。
  • 多进程:每个进程有独立的内存空间,不受GIL限制,适合CPU密集型任务,但进程之间的通信和资源共享较为复杂。

使用多进程

Python提供了multiprocessing模块,用于创建和管理多进程。它的API设计类似于threading模块,使得从多线程编程过渡到多进程编程变得更加容易。

start() 启动进程
join() 阻塞主进程,直到子进程完成
is_alive() 检查进程是否还在运行
terminate() 强制终止进程

使用multiprocessing模块创建进程的两种常见方式:

  1. 使用Process类:直接创建Process对象,并传入目标函数和参数

    import multiprocessing
    
    def task(name):
        print(f"Task {name} is running")
    
    if __name__ == '__main__':
        process1 = multiprocessing.Process(target=task, args=('A',))
        process2 = multiprocessing.Process(target=task, args=('B',))
    
        process1.start()
        process2.start()
    
        process1.join()
        process2.join()
    
  2. 继承Process类:通过继承multiprocessing.Process类并重写run()方法来定义进程的任务。

    import multiprocessing
    
    class MyProcess(multiprocessing.Process):
        def run(self):
            print(f"Process {self.name} is running")
    
    if __name__ == '__main__':
        process1 = MyProcess()
        process2 = MyProcess()
    
        process1.start()
        process2.start()
    
        process1.join()
        process2.join()
    

异步编程(Asyncio)

异步编程是一种并发编程的方式,它允许程序在执行耗时任务时不阻塞主线程,从而提高应用程序的性能和响应速度。与传统的多线程或多进程方式不同,异步编程通过事件循环(event loop)来调度任务的执行,使得程序能够在等待I/O操作时处理其他任务。

同步 vs 异步 vs 多线程

  1. 同步编程:任务按顺序执行,一个任务完成后才能执行下一个任务。如果任务需要等待I/O操作(如文件读写、网络请求),整个程序就会阻塞,直到该操作完成。

  2. 多线程编程:通过创建多个线程同时执行多个任务,从而提高并发性能。然而,线程之间共享内存可能导致数据竞争问题,需要复杂的锁机制来确保数据一致性。

  3. 异步编程:任务不会阻塞程序,而是通过事件循环管理任务的调度。当一个任务需要等待I/O操作时,事件循环可以执行其他任务。异步编程通常使用 async 和 await 关键字来定义和执行异步任务。

异步编程asyncio

asyncio 是Python的标准库,提供了异步编程的支持。它使得编写异步代码变得更简单和直观,通过 async 和 await 关键字,我们可以轻松定义异步函数并在事件循环中执行。

事件循环是 asyncio 的核心,负责调度和执行异步任务。一个事件循环在运行时,不断检查是否有任务需要执行,并在任务等待I/O操作时切换到其他任务。

import asyncio

async def main():
    print("Hello")
    await asyncio.sleep(1)
    print("World")

# 运行事件循环
asyncio.run(main())

在这个示例中,main 是一个异步函数,通过 await asyncio.sleep(1) 实现了非阻塞的等待。

异步函数使用 async def 来定义,返回一个协程对象(coroutine)。在异步函数中,你可以使用 await 关键字来等待另一个异步操作的完成。

import asyncio

async def fetch_data():
    print("Start fetching")
    await asyncio.sleep(2)  # 模拟耗时操作
    print("Done fetching")
    return {"data": 123}

async def main():
    result = await fetch_data()
    print(result)

asyncio.run(main())

await 的作用是暂停当前协程,等待 fetch_data 函数中的异步操作完成,然后继续执行。

常用的asyncio功能

并发运行多个任务
asyncio.gather 可以同时运行多个异步任务,并在所有任务完成后返回它们的结果。

import asyncio

async def task(name, delay):
    print(f"Task {name} started")
    await asyncio.sleep(delay)
    print(f"Task {name} finished")
    return name

async def main():
    results = await asyncio.gather(
        task("A", 2),
        task("B", 1),
        task("C", 3)
    )
    print(results)

asyncio.run(main())

在这个示例中,asyncio.gather 同时运行三个任务,并在它们完成后返回结果列表。

超时控制
有时,某些任务可能由于某些原因(如网络延迟)而需要花费很长时间。asyncio.wait_for 提供了一个超时机制,可以在超时时间内等待任务完成,否则会抛出 asyncio.TimeoutError。

import asyncio

async def long_task():
    await asyncio.sleep(5)
    return "Finished"

async def main():
    try:
        result = await asyncio.wait_for(long_task(), timeout=3)
        print(result)
    except asyncio.TimeoutError:
        print("Task timed out")

asyncio.run(main())

同步与异步代码的混合
在现实项目中,有时需要在异步代码中调用同步代码。asyncio.to_thread 可以将同步的阻塞代码转移到线程池中运行,以避免阻塞事件循环。

import asyncio
import time

def blocking_task():
    time.sleep(5)
    return "Blocking task result"

async def main():
    result = await asyncio.to_thread(blocking_task)
    print(result)

asyncio.run(main())

更多操作

Python也提供了额外的一些函数来提供辅助的能力

  1. 使用Lock对资源加锁,防止冲突访问
  2. 使用Queue实现不同线程/进程之间的数据通信
  3. 使用线程池Pool/进程池Pool,简化线程/进程的任务提交、等待结束、获取结果
  4. 使用subprocess启动外部程序的进程,并进行输入输出交互。

使用Queue进行进程间通信

在编程中,队列(Queue)是一种常用的数据结构,它遵循先进先出(FIFO, First-In-First-Out)的原则,即第一个进入队列的元素也是第一个被取出的元素。Python 提供了 queue 模块来支持多线程和多进程编程中的队列操作。Queue 是线程和进程安全的,因此在多线程和多进程环境下使用非常方便。

Queue 内部使用锁机制,确保在多线程/多进程环境下操作时不会发生数据竞争,通过 Queue 可以轻松实现线程/进程之间的数据传递,而不需要手动管理复杂的同步机制。

Queue模块的基本操作

Python 的 queue 模块提供了三个主要的类:

  • Queue:普通的 FIFO 队列。
  • LifoQueue:后进先出(LIFO)队列,类似于栈。
  • PriorityQueue:优先级队列,按照元素的优先级排序。
import queue

q = queue.Queue(maxsize=5)  # 创建一个最大容量为5的队列

q.put(1)  # 向队列中添加元素
q.put(2)
print(q.get())  # 从队列中取出元素,输出: 1
print(q.get())  # 输出: 2

print(q.empty())  # 检查队列是否为空,输出: True
  • put(item, block=True, timeout=None):将 item 放入队列。如果队列已满且 block 为 True,则阻塞等待直到有空间。
  • get(block=True, timeout=None):从队列中取出并返回一个元素。如果队列为空且 block 为 True,则阻塞等待直到有可用元素。
  • empty():检查队列是否为空。
  • full():检查队列是否已满。
  • qsize():返回队列中当前元素的数量。

LifoQueue 和 PriorityQueue

import queue

# LifoQueue 示例
lq = queue.LifoQueue()
lq.put(1)
lq.put(2)
print(lq.get())  # 输出: 2

# PriorityQueue 示例
pq = queue.PriorityQueue()
pq.put((1, 'task1'))  # 元组的第一个元素为优先级,数字越小优先级越高
pq.put((3, 'task3'))
pq.put((2, 'task2'))

print(pq.get())  # 输出: (1, 'task1')
print(pq.get())  # 输出: (2, 'task2')

Queue在多线程中的应用(实现生产者-消费者模式)

Queue 在多线程编程中非常有用,特别是用于实现生产者-消费者模式。在这种模式中,生产者线程将任务或数据放入队列,消费者线程从队列中取出任务并处理。

import threading
import queue
import time

def producer(q):
    for i in range(5):
        item = f'item-{i}'
        q.put(item)
        print(f'Produced {item}')
        time.sleep(1)

def consumer(q):
    while True:
        item = q.get()
        if item is None:  # 结束信号
            break
        print(f'Consumed {item}')
        q.task_done()

q = queue.Queue()

producer_thread = threading.Thread(target=producer, args=(q,))
consumer_thread = threading.Thread(target=consumer, args=(q,))

producer_thread.start()
consumer_thread.start()

producer_thread.join()

q.put(None)  # 发送结束信号
consumer_thread.join()

在这个示例中,生产者线程不断向队列中添加数据,而消费者线程从队列中取出数据进行处理。队列中的数据处理完毕后,通过发送 None 结束信号通知消费者线程停止。

注意:对于无限大的队列,要小心避免内存耗尽的风险。使用有界队列(设置 maxsize 参数)可以限制队列的最大大小。多进程应用同理。

线程安全锁LLock

线程安全指某个函数,函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正常完成。由于线程的执行随时会发生切换,就造成了不可预料的结果,出现线程不安全。为了避免这种情况,Python提供了线程安全锁机制,使得多个线程在访问共享资源时能够相互排斥,从而保证数据的一致性。

锁的基本概念

锁(Lock)是一种用于管理对共享资源访问的同步原语。它允许一次只有一个线程访问共享资源,其他线程必须等待,直到锁被释放为止。
轻松学Python并发编程:线程、进程与异步_第1张图片

import threading

lock = threading.Lock()

def critical_section():
    with lock:
        # 保护共享资源的代码块
        print("This is a critical section")

# 在多个线程中使用
thread1 = threading.Thread(target=critical_section)
thread2 = threading.Thread(target=critical_section)

thread1.start()
thread2.start()

thread1.join()
thread2.join()

with lock: 确保了每次只有一个线程可以进入临界区,防止多个线程同时修改共享资源。

线程池

在多线程编程中,创建和销毁线程是需要一定开销的,特别是在高频率地创建和销毁线程时,这种开销可能会显著影响程序的性能。线程池(Thread Pool)是一种优化机制,它通过提前创建好一组可复用的线程来管理并发任务的执行,避免了频繁创建和销毁线程的开销。

线程池ThreadPoolExecutor

在Python中,concurrent.futures 模块提供了 ThreadPoolExecutor 类来实现线程池。ThreadPoolExecutor 是一个非常方便的工具,它可以管理一个固定数量的线程,并提供简单的方法来提交任务和获取结果。

from concurrent.futures import ThreadPoolExecutor

使用 ThreadPoolExecutor 可以轻松创建线程池并提交任务。

from concurrent.futures import ThreadPoolExecutor
import time

def task(name):
    print(f"Task {name} is running")
    time.sleep(2)
    return f"Task {name} completed"

# 创建一个包含3个线程的线程池
with ThreadPoolExecutor(max_workers=3) as executor:
    futures = [executor.submit(task, i) for i in range(5)]
    
    for future in futures:
        print(future.result())

在这个示例中,我们创建了一个包含3个线程的线程池,并提交了5个任务。由于线程池最多只允许3个线程同时运行,所以这5个任务会分批执行。submit 方法将任务提交给线程池,返回一个 Future 对象。Future 表示异步执行的结果,可以通过 result() 方法获取任务的返回值。

线程池的基本操作

ThreadPoolExecutor 提供了几种重要的方法来管理线程和任务:

  • submit(fn, *args, **kwargs):提交一个任务到线程池中执行,并返回一个 Future 对象。
  • map(func, *iterables):类似于内置的 map() 函数,但它会将 func 分发到多个线程中并发执行。
  • shutdown(wait=True):停止线程池,不再接受新的任务。wait=True 表示等待所有线程执行完毕再关闭。
from concurrent.futures import ThreadPoolExecutor

def square(n):
    return n * n

with ThreadPoolExecutor(max_workers=4) as executor:
    results = executor.map(square, range(10))
    
    for result in results:
        print(result)

map 方法将一个可迭代对象中的每个元素传递给指定的函数,并将结果按顺序返回。与 submit 不同,map 是阻塞的,即它会等待所有任务执行完成后返回结果

演示

处理大量的网络请求

import requests
from concurrent.futures import ThreadPoolExecutor

urls = [
    'https://www.example.com',
    'https://www.python.org',
    'https://www.github.com',
    # 添加更多的URL
]

def fetch(url):
    response = requests.get(url)
    return url, response.status_code

with ThreadPoolExecutor(max_workers=5) as executor:
    results = executor.map(fetch, urls)
    
    for url, status in results:
        print(f"{url} returned status code {status}")

并行处理文件

import os
from concurrent.futures import ThreadPoolExecutor

def process_file(file_path):
    with open(file_path, 'r') as f:
        content = f.read()
        return file_path, len(content)

file_paths = [f"./files/file{i}.txt" for i in range(10)]

with ThreadPoolExecutor(max_workers=3) as executor:
    results = executor.map(process_file, file_paths)
    
    for file_path, length in results:
        print(f"{file_path} has {length} characters")

如有疑问,欢迎评论区留言!

你可能感兴趣的:(Python,python,开发语言,并发编程,线程,进程异步)