Python 并发编程:进程、线程与协程详解

部署运行你感兴趣的模型镜像

在现代软件开发中,充分利用计算机资源、提高程序执行效率是每个开发者追求的目标。Python 作为一门流行的编程语言,提供了多种并发编程方案,包括进程、线程和协程。本文将详细介绍这三种并发机制的概念、特点、使用方法以及适用场景,帮助读者理解并掌握 Python 并发编程的核心技术

一、多任务的概念​

多任务是指在同一时间内执行多个任务的能力。在日常生活中,我们经常同时做几件事情,比如一边听音乐一边写代码,这就是多任务的体现。在计算机领域,多任务同样重要,它可以:​

  • 提高程序执行效率​

  • 充分利用 CPU 资源​

  • 提升用户体验​

在 Python 中,实现多任务主要有三种方式:多进程、多线程和协程

二、进程的概念与使用​

2.1 什么是进程​

进程(Process) 是操作系统进行资源分配和调度运行的基本单位。通俗地理解,一个正在运行的程序就是一个进程。例如:​

  • 正在运行的 QQ​
  • 打开的浏览器​
  • 运行中的 Python 脚本

重要特性:​

  • 进程是资源分配的最小单位​
  • 一个程序运行后至少有一个进程​
  • 每个进程拥有独立的内存空间

2.2 多进程的优势​

使用多进程可以实现真正的并行执行,充分利用多核 CPU 的优势:​

  • 未使用多进程:程序默认创建一个主进程,所有任务在这个进程中串行执行​
  • 使用多进程:主进程可以创建多个子进程,多个任务并行执行

2.3 Python 中的多进程实现​

Python 提供了 multiprocessing 模块来创建和管理进程。​

基本使用步骤:​

  1. 导入模块:import multiprocessing​
  2. 创建进程对象:process = multiprocessing.Process (target=任务函数)​
  3. 启动进程:process.start ()

代码示例:

import multiprocessing
import time

def music():
    """播放音乐的任务"""
    for i in range(3):
        print("正在播放音乐...")
        time.sleep(1)

def coding():
    """编写代码的任务"""
    for i in range(3):
        print("正在编写代码...")
        time.sleep(1)

if __name__ == "__main__":
    # 创建音乐进程
    music_process = multiprocessing.Process(target=music)
    # 创建编码进程
    coding_process = multiprocessing.Process(target=coding)
    
    # 启动进程
    music_process.start()
    coding_process.start()
    
    print("主进程继续执行...")

2.4 进程的参数传递​

进程可以执行带有参数的任务,支持位置参数和关键字参数:

def music(singer, times):
    """播放指定歌手的音乐多次"""
    for i in range(times):
        print(f"正在播放{歌手}的音乐...")
        time.sleep(1)

def coding(language, lines):
    """使用指定语言编写代码"""
    for i in range(lines):
        print(f"使用{language}编写第{i+1}行代码...")
        time.sleep(1)

if __name__ == "__main__":
    # 使用args传递位置参数
    music_process = multiprocessing.Process(target=music, args=("周杰伦", 3))
    # 使用kwargs传递关键字参数
    coding_process = multiprocessing.Process(target=coding, kwargs={"language": "Python", "lines": 5})
    
    music_process.start()
    coding_process.start()

2.5 进程编号管理​

每个进程都有唯一的编号,便于进程管理:​

  • pid:当前进程编号​
  • ppid:父进程编号
import os
import multiprocessing

def task():
    """获取进程信息的任务"""
    print(f"子进程:pid={os.getpid()}, ppid={os.getppid()}")

if __name__ == "__main__":
    print(f"主进程:pid={os.getpid()}")
    
    process = multiprocessing.Process(target=task)
    process.start()
    process.join()  # 等待子进程完成

2.6 进程应用注意事项

(1) 进程间不共享全局变量​

这是进程最重要的特性之一。每个子进程都会拷贝主进程的资源,因此:

import multiprocessing
import time

my_list = []

def write_data():
    """向列表中写入数据"""
    for i in range(5):
        my_list.append(i)
        time.sleep(0.1)
    print(f"写入完成后的列表:{my_list}")

def read_data():
    """读取列表中的数据"""
    time.sleep(1)  # 等待写入完成
    print(f"读取到的列表:{my_list}")

if __name__ == "__main__":
    write_process = multiprocessing.Process(target=write_data)
    read_process = multiprocessing.Process(target=read_data)
    
    write_process.start()
    read_process.start()
    
    write_process.join()
    read_process.join()
    
    print(f"主进程中的列表:{my_list}")

运行结果分析:​

  • 写入进程的列表:[0, 1, 2, 3, 4]​
  • 读取进程的列表:[](空列表)​
  • 主进程的列表:[](空列表)​

这说明每个进程操作的都是自己独立的变量副本

(2) 主进程与子进程的结束顺序​

默认情况下,主进程会等待所有子进程完成后再结束。如果希望主进程结束时子进程也自动结束,可以设置守护进程:

import multiprocessing
import time

def long_running_task():
    """长时间运行的任务"""
    for i in range(10):
        print(f"子进程运行中... {i}")
        time.sleep(1)

if __name__ == "__main__":
    # 创建守护进程
    process = multiprocessing.Process(target=long_running_task)
    process.daemon = True  # 设置为守护进程
    process.start()
    
    print("主进程执行完毕,即将退出...")
    time.sleep(3)  # 主进程等待3秒后退出

三、线程的概念与使用​

3.1 什么是线程​

线程(Thread) 是程序执行的最小单位,它是进程中的一个实体。线程自己不拥有系统资源,只需要一点儿在运行中必不可少的资源,但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。​

形象比喻:​

  • 进程就像一个 QQ 软件​
  • 线程就像 QQ 中的聊天窗口​
  • 一个 QQ(进程)可以打开多个聊天窗口(线程)

3.2 为什么使用多线程​

相比多进程,多线程具有以下优势:​

  • 资源开销小:创建线程比创建进程消耗的资源少​
  • 启动速度快:线程启动比进程启动更快​
  • 通信简单:同一进程内的线程共享内存空间

3.3 Python 中的多线程实现​

Python 提供了threading模块来创建和管理线程。​

基本使用步骤:​

  1. 导入模块:import threading​
  1. 创建线程对象:thread = threading.Thread(target=任务函数)​
  1. 启动线程:thread.start()

代码示例:

import threading
import time

def music():
    """播放音乐的任务"""
    for i in range(3):
        print("正在播放音乐...")
        time.sleep(1)

def coding():
    """编写代码的任务"""
    for i in range(3):
        print("正在编写代码...")
        time.sleep(1)

if __name__ == "__main__":
    # 创建线程
    music_thread = threading.Thread(target=music)
    coding_thread = threading.Thread(target=coding)
    
    # 启动线程
    music_thread.start()
    coding_thread.start()
    
    print("主线程继续执行...")

3.4 线程的参数传递​

与进程类似,线程也支持参数传递:

import threading
import time

def music(singer, times):
    """播放指定歌手的音乐"""
    for i in range(times):
        print(f"正在播放{歌手}的音乐...")
        time.sleep(1)

def coding(language, lines):
    """使用指定语言编写代码"""
    for i in range(lines):
        print(f"使用{language}编写代码...")
        time.sleep(1)

if __name__ == "__main__":
    # 创建带参数的线程
    music_thread = threading.Thread(target=music, args=("周杰伦", 3))
    coding_thread = threading.Thread(target=coding, kwargs={"language": "Python", "lines": 5})
    
    music_thread.start()
    coding_thread.start()

3.5 线程的执行顺序​

线程的执行顺序是不确定的,由 CPU 调度决定:

import threading
import time

def task(thread_num):
    """线程执行的任务"""
    time.sleep(0.5)  # 模拟任务执行时间
    print(f"线程{thread_num}执行完成")

if __name__ == "__main__":
    print("开始创建线程...")
    
    # 创建10个线程
    threads = []
    for i in range(10):
        thread = threading.Thread(target=task, args=(i,))
        threads.append(thread)
        thread.start()
    
    # 等待所有线程完成
    for thread in threads:
        thread.join()
    
    print("所有线程执行完成")

运行结果特点:​

  • 线程的执行顺序是随机的​
  • 每次运行的结果可能不同​
  • 这体现了线程调度的不确定性

3.6 线程间共享全局变量​

这是线程与进程的重要区别。同一进程内的线程共享全局变量:

import threading
import time

my_list = []

def write_data():
    """向列表中写入数据"""
    for i in range(5):
        my_list.append(i)
        time.sleep(0.1)
    print(f"写入线程的列表:{my_list}")

def read_data():
    """读取列表中的数据"""
    time.sleep(1)  # 等待写入完成
    print(f"读取线程的列表:{my_list}")

if __name__ == "__main__":
    write_thread = threading.Thread(target=write_data)
    read_thread = threading.Thread(target=read_data)
    
    write_thread.start()
    read_thread.start()
    
    write_thread.join()
    read_thread.join()
    
    print(f"主线程中的列表:{my_list}")

运行结果分析:​

  • 写入线程的列表:[0, 1, 2, 3, 4]​
  • 读取线程的列表:[0, 1, 2, 3, 4]​
  • 主线程的列表:[0, 1, 2, 3, 4]​

这说明所有线程操作的是同一个全局变量

四、进程与线程的对比分析​

4.1 核心区别对比

特性

进程

线程

定义

操作系统分配资源的基本单位

进程内的执行单元,共享进程资源

资源开销

高(独立内存、资源)

低(共享进程内存)

启动速度

并发性

多核并行(真正并行)

伪并行(受 GIL 限制)

数据共享

需 IPC 机制(队列、管道等)

直接共享内存(需同步机制)

容错性

高(进程崩溃不影响其他进程)

低(线程崩溃可能导致进程终止)

适用场景

CPU 密集型任务

I/O 密集型任务

Python 模块

multiprocessing

threading

4.2 关系对比​

  1. 依附关系:线程是依附在进程里面的,没有进程就没有线程​
  2. 创建关系:一个进程默认提供一条线程,进程可以创建多个线程​
  3. 资源关系:进程分配资源,线程使用资源

4.3 优缺点分析​

进程优缺点:​

优点:​

  • 可以充分利用多核 CPU​
  • 进程间相互独立,容错性高​
  • 适合 CPU 密集型任务​

缺点:​

  • 创建和切换开销大​
  • 进程间通信复杂​
  • 内存占用高

线程优缺点:​

优点:​

  • 创建和切换开销小​
  • 线程间通信简单​
  • 内存占用低​

缺点:​

  • Python 中受 GIL 限制,不能真正并行​
  • 线程间共享数据需要同步​
  • 一个线程崩溃可能影响整个进程

4.4 GIL(全局解释器锁)的影响​

GIL 是什么:​

GIL 是 Python 解释器中的一个互斥锁,它确保同一时刻只有一个线程在执行 Python 字节码

对多线程的影响:​

  • 在 CPU 密集型任务中,多线程无法利用多核优势​
  • 在 I/O 密集型任务中,由于线程会等待 I/O 操作,多线程仍然有效​

解决方案:​

  • CPU 密集型任务:使用多进程​
  • I/O 密集型任务:使用多线程或协程

五、协程:轻量级并发​

5.1 什么是协程​

协程(Coroutine) 是用户态的轻量级线程,通过协作式多任务实现并发。相比线程,协程的切换无需操作系统调度,仅需保存寄存器上下文,因此效率更高。​

核心特点:​

  • 轻量级:比线程更轻量,创建成本更低​
  • 协作式:协程主动让出 CPU 控制权​
  • 单线程:在一个线程内实现并发​
  • 高并发:单线程内可处理数千个协程

5.2 协程的优势​

  1. 无锁机制:避免多线程同步开销​
  2. 高并发:单线程内处理数千级 I/O 密集型任务​
  3. 代码简洁:用同步语法写异步逻辑​
  4. 内存高效:大量协程占用内存少

5.3 Python 中的协程实现​

Python 3.5 + 引入了asyncio模块,提供了原生的协程支持。​

基本语法:

import asyncio

async def task1():
    """协程任务1"""
    for i in range(3):
        print("任务1执行...")
        await asyncio.sleep(1)  # 非阻塞等待

async def task2():
    """协程任务2"""
    for i in range(3):
        print("任务2执行...")
        await asyncio.sleep(1)

async def main():
    """主协程"""
    # 并发执行多个协程
    await asyncio.gather(task1(), task2())

# 运行主协程
if __name__ == "__main__":
    asyncio.run(main())

关键概念:​

  • async def:定义协程函数​
  • await:暂停协程执行,等待其他协程完成​
  • asyncio.run():运行主协程​
  • asyncio.gather():并发执行多个协程

5.4 协程的核心 API

API

功能描述

asyncio.create_task()

将协程包装为任务对象

asyncio.gather()

并发执行多个协程

asyncio.sleep()

非阻塞式等待

asyncio.Queue

协程安全队列

asyncio.Lock

协程锁

5.5 协程的实际应用:异步爬虫​

协程特别适合 I/O 密集型任务,如网络请求:

import asyncio
import aiohttp

async def fetch_url(session, url):
    """异步获取网页内容"""
    async with session.get(url) as response:
        content = await response.text()
        return f"URL: {url}, 状态码: {response.status}, 长度: {len(content)}"

async def main():
    """主协程:并发爬取多个网页"""
    urls = [
        "https://www.baidu.com",
        "https://www.taobao.com", 
        "https://www.jd.com",
        "https://www.zhihu.com",
        "https://www.github.com"
    ]
    
    async with aiohttp.ClientSession() as session:
        # 创建任务列表
        tasks = [fetch_url(session, url) for url in urls]
        # 并发执行所有任务
        results = await asyncio.gather(*tasks)
        
        # 打印结果
        for result in results:
            print(result)

if __name__ == "__main__":
    asyncio.run(main())

性能优势:​

  • 相比同步爬虫,效率提升 10 倍以上​
  • 单线程内可同时处理数百个网络请求​
  • 避免了线程切换的开销

5.6 其他协程库​

除了asyncio,Python 还有其他优秀的协程库:

(1)Gevent

from gevent import monkey
import gevent
import time

# 打补丁,使标准库支持协程
monkey.patch_all()

def task(name):
    """协程任务"""
    for i in range(3):
        print(f"任务{name}执行... {i}")
        time.sleep(1)

def main():
    """主函数"""
    # 创建协程
    g1 = gevent.spawn(task, "A")
    g2 = gevent.spawn(task, "B")
    g3 = gevent.spawn(task, "C")
    
    # 等待所有协程完成
    gevent.joinall([g1, g2, g3])

if __name__ == "__main__":
    main()

(2)Greenlet

from greenlet import greenlet
import time

def test1():
    """第一个协程"""
    while True:
        print("test1")
        gr2.switch()  # 切换到test2
        time.sleep(1)

def test2():
    """第二个协程"""
    while True:
        print("test2")
        gr1.switch()  # 切换到test1
        time.sleep(1)

if __name__ == "__main__":
    # 创建协程对象
    gr1 = greenlet(test1)
    gr2 = greenlet(test2)
    
    # 启动第一个协程
    gr1.switch()

5.7 协程使用注意事项​

  1. 避免阻塞操作:协程内禁用time.sleep()等同步 IO 操作,应使用asyncio.sleep()​
  2. CPU 密集型任务:协程不适合 CPU 密集型任务,需结合多进程​
  3. 异常处理:需要妥善处理协程中的异常​
  4. 调试困难:协程的调试比线程更复杂

六、并发编程的选择策略​

6.1 任务类型分析​

CPU 密集型任务​

  • 特点:大量计算,CPU 使用率高​
  • 推荐方案:多进程​
  • 示例:数据分析、科学计算、图像处理​

I/O 密集型任务​

  • 特点:大量等待时间(网络请求、文件读写)​
  • 推荐方案:协程 > 多线程​
  • 示例:爬虫、API 服务、文件处理

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值