Python并发编程

Python并发编程

多线程多进程 是操作系统层面的并发,而多协程 是用户层面的并发。多线程 是一个进程内的多个执行流,多进程 是多个独立的进程,而多协程是在单个线程内实现的轻量级任务切换。

怎么选择?

  1. 什么是I/O密集型任务和CPU密集型任务?
  • I/O密集型任务:主要消耗时间在等待I/O操作(读取磁盘、内存、网络)完成上,如文件读写、网络请求等。
  • CPU密集型任务:主要消耗时间在计算上,如复杂的数学运算、图像处理、压缩解压缩等。
  1. 多进程、多线程和多协程的对比

一个进程中可以包含多个线程,而每个线程可以包含多个协程。

  • 多线程:相比于多进程 开销小;但是由于GIL的存在,只能并发执行,不能利用多CPU;相对于协程,启数目有限制,占用内存较多,开销较大。适用于I/O密集型任务。
  • 多进程:可以充分利用多CPU;占用资源最多可启动数目比线程小。适用于CPU密集型任务。
  • 多协程:相比于线程和进程,开销最小可启动数目最多;但是支持的库有限制,代码编写复杂。适用于I/O密集型任务,适用于超多任务运行的场景。

全局解释器锁(GIL)

python速度慢的两大原因

  1. 解释型语言:Python是一种解释型语言,代码在运行时需要经过解释器逐行解释执行,相比于编译型语言,执行速度较慢。
  2. 全局解释器锁(GIL):Python的多线程实现中存在全局解释器锁(GIL),它限制了同一时刻只有一个线程可以执行Python字节码,导致多线程无法充分利用多核CPU的优势。

GIL是什么

全局解释器锁。它使得任何时刻仅有一个线程在执行。即便是在多核CPU上运行的Python程序,也只能有一个线程在某一时刻执行。

为什么会有GIL

GIL的设计初衷是为了简化内存管理和垃圾回收机制,避免多线程同时访问共享资源时出现数据不一致的问题。通过引入GIL,Python解释器可以确保在任何时刻只有一个线程在执行,从而避免了复杂的锁机制,提高了单线程程序的性能。

怎么规避GIL的影响

  1. 使用多线程:对于I/O密集型任务,多线程可以有效地利用等待I/O操作的时间,提升程序的并发性能。但是多线程用于CPU密集型任务时,由于需要频繁切换线程,反而会降低性能。
  2. 使用多进程:对于CPU密集型任务,可以使用多进程来充分利用多核CPU的优势。每个进程都有自己的Python解释器和内存空间,避免了GIL的限制。

多线程(threading模块)

基础语法示例代码

python 复制代码
import threading
import requests
import time

url = [
    f"https://www.cnblogs.com/#p{page}" for page in range(1, 50)
]
def crawl(url: str):
    response = requests.get(url)#发送HTTP GET请求
    print(url, len(response.text))

def single_thread_crawl():
    print("单线程爬取开始")
    begin_time = time.time()
    for u in url:
        crawl(u)
    end_time = time.time()
    print("单线程爬取结束")
    print(f"单线程爬取耗时: {end_time - begin_time} 秒")

def multi_thread_crawl():
    print("多线程爬取开始")
    begin_time = time.time()
    threads = []
    for u in url:
        threads.append(
            threading.Thread(target=crawl, args=(u,))
        )
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    end_time = time.time()
    print("多线程爬取结束")
    print(f"多线程爬取耗时: {end_time - begin_time} 秒")

single_thread_crawl()
multi_thread_crawl()

多组件pipeline技术架构

多组件pipeline技术架构是一种将复杂任务拆分为多个独立组件,并通过管道(pipeline)连接这些组件以实现数据流动和处理的架构模式。这种架构通常用于数据处理、图像处理、音频处理等领域,能够提高系统的可维护性、可扩展性和性能。

  • 每个组件负责特定的任务,如数据采集、预处理、分析、存储等。
  • 组件之间通过管道连接,数据从一个组件流向下一个组件。

多线程数据通信(queue.Queue)

在多线程编程中,线程之间需要进行数据通信和共享。Python的queue模块提供了一个线程安全的队列类,可以方便地实现多线程之间的数据通信。

接下来是一个使用queue.Queue实现多线程数据通信的示例代码:

我是要展示一个生产者-消费者模型的例子,其中一个线程负责生产数据,另一个线程负责消费数据。

blog_spider.py

python 复制代码
import threading
import requests
import time
from bs4 import BeautifulSoup

url = [
    f"https://www.cnblogs.com/#p{page}" for page in range(1, 10)
]
def crawl(url: str):#用于爬取指定URL的网页内容
    response = requests.get(url)#发送HTTP GET请求
    return response.text#返回网页的HTML内容

def parse(html):
    soup = BeautifulSoup(html, "html.parser")#两个参数,第一个是html内容,第二个是解析器,html.parser意思是使用Python内置的HTML解析器
    titles = soup.find_all("a", class_="post-item-title")#获取所有class属性为post-item-title的a标签
    return [(t["href"], t.get_text()) for t in titles]

producer_consumer.py

python 复制代码
import queue #queue模块提供了同步的队列类,用于多线程编程中的线程间通信
import blog_spider
import time
import random
import threading

def do_crawl(url_queue: queue.Queue, html_queue: queue.Queue):
    while True:
        url = url_queue.get()
        if url is None:
            url_queue.task_done()
            break
        html = blog_spider.crawl(url)
        html_queue.put(html)
        print(threading.current_thread().name, 
              f"爬取完成: {url}",
              "url_queue大小=", url_queue.qsize(),
        )
        url_queue.task_done()
        time.sleep(random.randint(1,2))

    
def do_parse(html_queue:queue.Queue,fout):
    while True:
        html = html_queue.get()
        if html is None:
            html_queue.task_done()
            break
        results = blog_spider.parse(html)
        for result in results:
            fout.write(str(result) + "\n")#写入文件,fout是一个文件对象
        print(threading.current_thread().name, 
              f"result大小: {len(results)}",
              "html_queue大小=", html_queue.qsize(),
        )
        html_queue.task_done()
        time.sleep(random.randint(1,2))

if __name__ == '__main__':
    url_queue = queue.Queue()
    html_queue = queue.Queue()

    for url in blog_spider.url:
        url_queue.put(url)
    
    crawl_threads = []
    parse_threads = []

    for idx in range(3):
        t = threading.Thread(
            target=do_crawl,#target参数指定线程要执行的函数
            args=(url_queue, html_queue),#args参数传递给目标函数的参数
            name=f"爬虫线程-{idx}"
        )
        t.start()
        crawl_threads.append(t)
        
    fout = open("producer_consumer_result.txt", "w", encoding="utf-8")
    for idx in range(2):
        t = threading.Thread(
            target=do_parse,
            args=(html_queue, fout),
            name=f"解析线程-{idx}"
        )
        t.start()
        parse_threads.append(t)

    url_queue.join()  # 等待所有任务完成
    print("所有URL爬取完成")
    for _ in range(3):
        url_queue.put(None)  # 发送结束信号给爬取线程
    for t in crawl_threads:
        t.join()
    html_queue.join()  # 等待所有任务完成
    print("所有HTML解析完成")
    for _ in range(2):
        html_queue.put(None)  # 发送结束信号给解析线程
    for t in parse_threads:
        t.join()
    fout.close()
    print("所有线程已结束")

线程锁

线程安全是指某个函数、函数库在多线程环境下被调用,能够正确的处理多个线程的共享变量,使程序正确完成。

因此,可以使用Lock对象来实现线程安全,确保同一时刻只有一个线程能够访问共享资源,从而避免数据竞争和不一致的问题。

python 复制代码
import time
import threading

lock = threading.Lock()

class Account:
    def __init__(self,banlance):
        self.banlance=banlance

def draw(account,amount):
    with lock:#是lock.acquire()和lock.release()的简写形式
        if(account.banlance>=amount):
            time.sleep(0.1)
            print(threading.current_thread().name+"取钱成功!取出金额:"+str(amount))
            account.banlance-=amount
            print(threading.current_thread().name+"余额为:"+str(account.banlance))
        else:
            print(threading.current_thread().name+"余额不足,取款失败!")

if __name__ == '__main__':
    account = Account(1000)
    ta = threading.Thread(name='ta', target=draw, args=(account,800))
    tb = threading.Thread(name='tb', target=draw, args=(account,800))
    ta.start()
    tb.start()
    ta.join()
    tb.join()

线程池

线程池是一种管理和复用线程的技术,可以避免频繁创建和销毁线程带来的开销,提高程序的性能和响应速度。Python的concurrent.futures模块提供了一个方便的线程池实现,可以使用ThreadPoolExecutor类来创建和管理线程池。

  1. map(): 将一个可迭代对象中的每个元素传递给指定的函数,并返回一个包含结果的迭代器。
  2. submit() : 提交一个可调用对象(函数)到线程池,并返回一个Future对象,表示该任务的执行结果。

多进程(multiprocessing模块)

多进程是指在操作系统层面上同时运行多个进程,每个进程都有自己的内存空间和资源。Python的multiprocessing模块提供了一个简单的接口来创建和管理进程。

多进程基础语法

Process类参数:

  1. target:指定进程要执行的函数。
  2. args:传递给目标函数的参数,以元组的形式传递。
  3. kwargs:传递给目标函数的关键字参数,以字典的形式传递。
    方法:
  4. start():启动进程,调用目标函数。
  5. join():等待进程结束,阻塞当前线程直到进程完成。
  6. is_alive(): 检查进程是否仍在运行,返回布尔值。
    属性
  7. name:进程的名称,可以在创建进程时指定,默认值为"Process-N",其中N是一个递增的整数。
  8. pid:进程的ID,由操作系统分配的唯一标识符。
python 复制代码
import time
from multiprocessing import Process
import os

def coding(name,num):
    print(os.getpid())#获取当前进程ID
    print(os.getppid())#获取父进程ID
    for i in range(1,num + 1):
        print(f'{name}正在敲第{i}行代码')
        time.sleep(0.05)

def music(name,num):
    print(os.getpid())
    print(os.getppid())
    for i in range(1,num + 1):
        print(f'{name}正在听第{i}首音乐')


if __name__ == '__main__':
    p1 = Process(target=coding, args=('小明', 10),name="小明的编码进程")  # 方式一:args方式传参
    p2 = Process(target=music, kwargs={'name':'小红', 'num':10},name="小红的音乐进程")  # 方式二:kwargs方式传参
    p1.start()
    p2.start()
    print(p1.name," ",p1.pid)
    print(p2.name," ",p2.pid)
    p1.join()
    p2.join()
    print(f"主进程的父进程:{os.getppid()}")#pycharm运行时,主进程的父进程是pycharm的进程ID

多进程的特点

数据隔离

进程之间的数据是相互隔离的,每个进程都有自己的内存空间,无法直接访问其他进程的内存数据。

如果需要在进程之间共享数据,可以使用multiprocessing模块提供的QueuePipe等通信机制,或者使用共享内存(ValueArray)来实现数据共享。

python 复制代码
list = []
def producer():
    for i in range(10):
        list.append(i)
        print(f"生产者生产了数据: {i}")

def consumer():
    while True:
        if list:
            item = list.pop(0)
            print(f"消费者消费了数据: {item}")

if __name__ == '__main__':
    p1 = Process(target=producer)
    p2 = Process(target=consumer)
    p1.start()
    p2.start()
    p1.join()
    print("生产者进程结束")
    p2.join()
    print("消费者进程结束")

上面的代码输出只会输出生产者生产的数据,而消费者进程不会消费任何数据,因为两个进程的list变量是独立的,互不影响。

python 复制代码
from multiprocessing import Process, Queue

queue = Queue()
def producer(q: Queue):
    for i in range(5):
        q.put(i)
        print(f"生产者生产了数据: {i}")
        
def consumer(q: Queue):
    while True:
        item = q.get()
        if item is None:
            break
        print(f"消费者消费了数据: {item}")
        
if __name__ == '__main__':
    p1 = Process(target=producer, args=(queue,))
    p2 = Process(target=consumer, args=(queue,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()

上面的代码中,使用Queue实现了进程间的数据共享,生产者进程将数据放入队列中,消费者进程从队列中获取数据进行消费。

主进程等待子进程结束再结束

在多进程编程中,主进程通常会等待所有子进程结束后再退出。可以使用join()方法来实现这一点。

python 复制代码
if __name__ == '__main__':
    p1 = Process(target=coding, args=('小明', 10))
    p2 = Process(target=music, args=('小红', 10))
    p1.start()
    p2.start()
    p1.join()  # 主进程等待p1结束
    p2.join()  # 主进程等待p2结束
    print("所有子进程结束,主进程退出")

如果想让主进程结束之后子进程继续运行,可以将子进程设置为守护进程(daemon)。

python 复制代码
if __name__ == '__main__':
    p1 = Process(target=coding, args=('小明', 10))
    p2 = Process(target=music, args=('小红', 10))
    p1.daemon = True  # 设置为守护进程
    p2.daemon = True  # 设置为守护进程
    p1.start()
    p2.start()
    print("主进程结束,子进程作为守护进程也会结束")

这样,主进程结束后,守护进程也会随之结束,不会继续运行。

多协程(asyncio模块)

多协程是指在单个线程内实现的轻量级任务切换,通过协程可以在等待I/O操作时切换到其他任务,从而提高程序的并发性能。

Python的asyncio模块提供了一个强大的异步编程框架,可以方便地实现多协程编程。

Greenlet库

Greenlet是一个轻量级的协程库,可以在单个线程内实现多协程的切换。Greenlet通过手动切换协程来实现并发执行,适合I/O密集型任务。

python 复制代码
from greenlet import greenlet

def foo():
    print("foo")
    g2.switch()
    print("foo again")
    g2.switch()

def bar():
    print("bar")
    g1.switch()
    print("bar again")

if __name__ == '__main__':
    g1 = greenlet(foo)
    g2 = greenlet(bar)
    g1.switch()

Asyncio模块

asyncio是Python内置的异步编程模块,提供了事件循环、协程、任务等功能,可以方便地实现多协程编程。适合I/O密集型任务。

基本使用
python 复制代码
import asyncio
import time

async def task(i):
    print(f"Task {i} is starting")
    await asyncio.sleep(i) #模拟IO操作
    print(f"Task {i} is completed")
    return pow(i, 2)

def task_callback(obj):
    print(f"Task {obj} is completed, the result is {obj.result()}")

start = time.time()
# 1) 构建一个事件循环
loop = asyncio.get_event_loop()#获取当前线程的事件循环,如果没有则创建一个新的事件循环
# 2) 创建协程对象
tasks = [
    asyncio.ensure_future(task(i)) for i in range(5)
]
for task in tasks:
    task.add_done_callback(task_callback)
# 3) 收集任务并等待
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print(f"All tasks completed in {end - start} seconds")
新版本语法支持
  1. **asyncio.run()**可以替换掉事件循环的创建和关闭,更加简洁。
  2. **asyncio.create_task()**可以替换掉asyncio.ensure_future(),更加直观。
  3. **asyncio.gather()**可以替换掉asyncio.wait(),更加方便地收集任务结果,并且还会返回任务的结果。
python 复制代码
import asyncio
import time

async def task(i):
    print(f"Task {i} is starting")
    await asyncio.sleep(i) #模拟IO操作
    print(f"Task {i} is completed")
    return pow(i, 2)

def task_callback(obj):
    print(f"Task {obj} is completed, the result is {obj.result()}")

async def main():
    start = time.time()
    # 创建协程对象
    tasks = [
        asyncio.create_task(task(i)) for i in range(5)
    ]
    for t in tasks:
        t.add_done_callback(task_callback)
    # 收集任务并等待
    result = await asyncio.gather(*tasks) # gather方法等待所有任务完成,tasks前面有*号表示将列表解包成单独的参数传递给gather函数
    print("Results:", result)
    end = time.time()
    print(f"All tasks completed in {end - start} seconds")

asyncio.run(main())
相关推荐
Victory_orsh4 小时前
“自然搞懂”深度学习系列(基于Pytorch架构)——02小试牛刀
人工智能·python·深度学习·神经网络·机器学习
Bruce-li__4 小时前
CI/CD流水线全解析:从概念到实践,结合Python项目实战
开发语言·python·ci/cd
2401_841495644 小时前
自然语言处理实战——英法机器翻译
人工智能·pytorch·python·深度学习·自然语言处理·transformer·机器翻译
gAlAxy...5 小时前
面试JAVASE基础(五)——Java 集合体系
java·python·面试·1024程序员节
夏玉林的学习之路5 小时前
Anaconda的常用指令
开发语言·windows·python
张可爱5 小时前
20251026-从网页 Console 到 Python 爬虫:一次 B 站字幕自动抓取的实践与复盘
前端·python
B站计算机毕业设计之家5 小时前
计算机视觉python口罩实时检测识别系统 YOLOv8模型 PyTorch 和PySide6界面 opencv (建议收藏)✅
python·深度学习·opencv·计算机视觉·cnn·1024程序员节
张较瘦_5 小时前
[论文阅读] 从 5MB 到 1.6GB 数据:Java/Scala/Python 在 Spark 中的性能表现全解析
java·python·scala
Xiaoweidumpb5 小时前
Linux Docker docker-compose 部署python脚本
linux·python·docker