赞
踩
多线程编程: 使用 threading
模块可以创建多个线程来并发执行任务。然而,Python 的全局解释锁(GIL)可能会限制多线程的并行性,尤其在 CPU 密集型任务上效果有限。
多线程示例:注意 我这里是使用的线程池
- import concurrent.futures
- import threading
- import time
-
- # 定义线程要执行的函数
- def worker(index):
- print(f"Thread {index} is starting")
- time.sleep(2)
- print(f"Thread {index} is done")
-
- start = time.time()
-
- # 使用线程池创建多线程
- with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
- futures = [executor.submit(worker, i) for i in range(5)]
- # 等待所有线程完成
- concurrent.futures.wait(futures)
-
- end = time.time()
- print("concurrent cost: ", end - start, " seconds")
-
- print("All threads are done")
-
多进程编程: 使用 multiprocessing
模块可以创建多个进程来并发执行任务。每个进程都有自己的解释器和独立的内存空间,可以有效地利用多核处理器,适用于 CPU 密集型任务。
- import concurrent.futures
- import time
-
- # 定义进程要执行的函数
- def worker(index):
- print(f"Process {index} is starting")
- # 模拟耗时操作
- time.sleep(2)
- print(f"Process {index} is done")
-
- # 使用进程池创建多进程
- # max_workers=3 表示创建一个最大容量为 3 的进程池,即同时可以运行的进程数量不会超过 3 个。
- with concurrent.futures.ProcessPoolExecutor(max_workers=3) as executor:
- # 提交多个进程任务 创建一个包含 5 个任务的列表,每个任务都会执行 worker 函数,传递不同的索引值作为参数。
- futures = [executor.submit(worker, i) for i in range(5)]
-
- # 等待所有进程完成
- concurrent.futures.wait(futures)
- print("All processes are done")
'运行
这两个示例都使用了 concurrent.futures
模块,它提供了线程池和进程池来简化并发编程。
异步编程: 使用 asyncio
库可以实现异步编程,使用协程来处理非阻塞式的 I/O 操作。异步编程适用于 I/O 密集型任务,可以大大提高程序的并发性能。
- import asyncio
- import aiohttp
-
-
- # 异步函数,用于从 URL 获取内容
- async def fetch_url(session, url):
- async with session.get(url) as response:
- return await response.text()
-
-
- # 主函数,用于并发发送多个 HTTP 请求
- async def main():
- urls = ['https://www.csdn.net/', 'https://www.baidu.com',]
-
- # 使用 aiohttp 创建异步会话
- async with aiohttp.ClientSession() as session:
- # 创建异步任务列表,每个任务会调用 fetch_url 函数,传递不同的 URL
- tasks = [fetch_url(session, url) for url in urls]
-
- # 并发执行所有异步任务,并等待结果
- results = await asyncio.gather(*tasks)
-
- # 打印每个 URL 的内容长度
- for url, result in zip(urls, results):
- print(f"Content from {url}: {len(result)} bytes")
-
-
- # 创建并运行事件循环
- loop = asyncio.get_event_loop()
- loop.run_until_complete(main()) # 运行主函数并等待其完成
- loop.close() # 关闭事件循环
线程池和进程池: 使用 concurrent.futures
模块可以创建线程池和进程池,提供了更高级别的接口来管理并发任务。
并行映射: 使用 map
函数的并行版本 map_async
可以在多个线程或进程中并行地执行函数。
并发原语: Python 提供了一些用于处理并发的原语,如锁、信号量、条件变量等,可以帮助你在多线程和多进程中管理共享资源。
第三方库: 还有一些第三方库如 joblib
、grequests
、gevent
等,提供了更多关于并发编程的功能和工具。
无论你选择哪种方式,都需要根据你的应用场景和需求来决定。每种方式都有其优势和适用范围。例如,多线程适用于 I/O 密集型任务,多进程适用于 CPU 密集型任务,异步编程适用于高并发的网络应用等。选择适当的并发策略可以提高程序的性能和响应能力。
场景1:一个网络爬虫,按顺序爬取花了一小时,采用并发下载减少到20 min,场景2:一个app 应用,优化前每次打开页面需要3s,采用异步并发提升到每次200ms;引入并发,就是为了提升程序运行的速度,有哪些程序提升速度的方法呢?
① 单线程串行:由CPU 和 IO 轮流执行;② 多线程并发(threading)③ 多CPU 并行(multiprocessing)④ 多机器并行;
多线程:threading,利用CPU 和 IO 可以同时执行的原理,让CPU 不会干巴巴等待IO 完成;
多进程:multiprocessing,利用多核CPU的能力,真正并行执行任务;
异步IO:asyncio,在单线程利用CPU 和 IO 同时执行的原理,实现函数的异步执行;
使用 Lock 对资源进行加锁,防止冲突访问;python 也提供了Queue 实现不同线程/进程之间的数据通信 ,实现生产者--消费者模式
使用线程池Pool/进程池Pool,简化线程/进程任务提交,等待结果、获取结果;
使用subprocess 启动外部程序的进程,并进行输入输出的交互;
python 并发编程有三种模式:1. 多线程Thread;2. 多进程Process;3. 多协程Corroutine;思考一下三个问题:1. 什么是CPU密集型计算,IO 密集型计算;2. 多线程,多进程和多协程的区别;3. 怎么样根据任务选择对应技术?
CPU 密集型(CPU-bound):CPU 密集型也叫做计算密集型,是指I/O在很短时间内就可以完成,CPU 需要大量的计算和处理,特点是CPU 占用率相当高;
例如:压缩解压缩、加密解密、正则表达式搜索、计算;
I/O密集型(I/O bound):IO 密集型指的是系统运作大部分的状况是CPU 在等I/O(硬盘,内存)的读写操作,CPU 占用率较低,
例如:文件处理程序,网络爬虫程序,读写数据库程序、网络请求;
一个进程中可以启动 N 个进程,一个线程中可以启动 N 个协程;多进程,多线程,多协程三种技术中只有多进程能够同时利用多核cpu并行计算;
多线程 Thread (threading):优点:相比进程,更加轻量级,占用资源较少;缺点:相比进程:多线程只能够并发执行,不能够利用多CPU(GIL),这是python 多线程一个很大的缺点,同一时间只能够使用一个cpu,相比于协程:启动数目有限,占用内存资源,有线程切换开销,多线程适用于IO密集行型计算,同时运行的任务数目要求不多;
多进程 Process (multiprocessing):优点:可以利用多核cpu 并行计算;缺点:占用资源最多,可以启动的数目比线程要少;适用于cpu 密集型计算;
大量并发: 当需要同时处理大量任务的时候,特别是IO密集型任务,使用协程可以减少线程/进程切换的开销,提高性能。
异步IO: 对于需要频繁进行异步IO操作的任务,使用协程可以提供高效的异步处理方式。
高并发服务器: 在构建高并发的服务器应用时,协程可以实现非常高的并发能力,例如Web框架、实时聊天服务器等。
三. python 的全局解释器锁(GIL)
1. python 速度慢的两大原因
相比对c++/java,python 确实比较慢,在一些特殊场景下,python 比c++ 慢100~200倍;由于速度慢的原因,很多公司的基础架构代码仍然使用c/c++开发。python 速度慢的两大原因:① python 属于动态类型语言,边解释边执行;② python 中由于存在GIL,无法利用多核cpu 并发执行。
2. GIL 是什么?
全局解释器锁(GIL):是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻只有一个线程在执行;即使在多核处理器上,使用GIL 的解释器也只允许同一时间执行一个线程;
所以当GIL存在的时候,即使电脑有多核cpu,单个时刻也只能够执行1个,相比并发加速的c++/java 所以慢;
3. 为什么会有GIL 这个东西?
简而言之,python 设计初期,为了规避并发问题引入了GIL,为了解决多线程之间数据完整性和状态同步问题;python 中对象的管理,是使用引用计数器进行的,引用数为0则释放对象,但是GIL 确实有好处,简化了python对于共享资源的管理;
4. 怎么样规避GIL 带来的限制?
多线程 threading 机制依然是有用的,用于IO 密集型计算,因为在IO期间,线程会释放GIL,实现CPU 和IO的并行,因此多线程用于IO密集型计算依然可以大幅度提升速度,但是多线程用于CPU密集型计算的时候,只会更加拖慢速度;使用multiprocessing 的多进程机制实现并行计算,利用多核cpu 的优势,为了应对GIL 的问题,python 提供了multiprocessing。
1. python 创建线程的方法;
① 准备一个函数: func (a,b);② 怎样创建一个线程;threading.Thread(target=func, args=(100,200)); ③ 启动线程:t.start();4. 等待结束:t.join();
并不是所有的库都支持异步爬虫,例如requests 库就不支持异步IO特性:
- import asyncio
- import time
- import aiohttp
- import blog_spider # 假设这是一个包含urls的模块
-
- # 协程函数,用于异步地爬取网页内容
- async def async_craw(url: str):
- print("craw url: ", url)
- async with aiohttp.ClientSession() as session:
- async with session.get(url) as resp:
- result = await resp.text() # 异步等待获取响应内容
- print(f"craw url:{url}, {len(result)}")
-
- # 获取事件循环
- loop = asyncio.get_event_loop()
-
- # 创建多个协程任务
- tasks = [loop.create_task(async_craw(url)) for url in blog_spider.urls]
-
- start = time.time() # 记录开始时间
- # 并发执行所有协程任务,并等待完成
- loop.run_until_complete(asyncio.wait(tasks))
- end = time.time() # 记录结束时间
-
- # 打印执行时间
- print("use time seconds: ", end - start)
async with aiohttp.ClientSession() as session:
:这里创建了一个异步的网络会话(session)。使用 async with
上下文管理器可以确保在使用完后自动释放资源,避免资源泄露。
async with session.get(url) as resp:
:在异步会话中,使用 session.get(url)
发起异步的HTTP GET请求,得到一个异步的响应对象 resp
。使用 async with
上下文管理器可以确保在使用完后自动关闭响应连接。
result = await resp.text()
: 这里使用 await
关键字异步等待获取响应内容。resp.text()
是一个异步方法,它返回响应的文本内容。使用 await
表示当前协程会等待这个异步操作完成,然后再继续执行。
print(f"craw url:{url}, {len(result)}")
:在获取了响应内容后,这里打印出爬取的URL和响应内容的长度。
信号量(Semaphore)是一种用于控制对共享资源的访问的同步机制。它可以用于多线程、多进程以及异步编程中,用于限制对共享资源的并发访问数量。信号量在并发编程中具有以下主要用途:
控制并发度: 信号量可以限制同时访问某个共享资源的数量。通过设置信号量的初始值,你可以控制允许同时执行的线程、进程或协程的数量。这对于避免资源竞争、提高效率以及防止资源耗尽都非常有用。
解决生产者-消费者问题: 在多线程或多进程环境中,生产者和消费者之间的协作需要一种机制来保证数据的正确传递和处理。信号量可以用于解决生产者-消费者问题,确保生产者和消费者之间的同步和协作。
限制资源访问: 信号量可以用于限制对一些有限资源的访问,例如数据库连接、文件句柄等。通过控制同时访问这些资源的数量,可以避免资源被过多的线程或进程占用而导致资源耗尽。
实现互斥: 在多线程或多进程环境中,信号量可以用于实现互斥,即一次只允许一个线程或进程访问某个共享资源。这有助于避免竞争条件和数据不一致性。
实现条件等待: 信号量可以与条件变量结合使用,用于实现条件等待。当某个条件不满足时,线程可以等待信号量,直到条件满足后才继续执行
- import asyncio
- import time
- import aiohttp
- import blog_spider
-
- # 控制并发度,限制同时进行的异步请求数量为 10
- semaphore = asyncio.Semaphore(10)
-
- # 协程函数,用于异步地爬取网页内容
- async def async_craw(url: str):
- # 在 semaphore 代码块内的操作受到 semaphore 信号量的控制,限制并发数量
- async with semaphore:
- print("craw url: ", url)
- async with aiohttp.ClientSession() as session:
- async with session.get(url) as resp:
- result = await resp.text()
- # 模拟耗时操作,休眠 5 秒
- await asyncio.sleep(5)
- print(f"craw url:{url}, {len(result)}")
-
- loop = asyncio.get_event_loop()
-
- # 创建多个协程任务
- tasks = [loop.create_task(async_craw(url)) for url in blog_spider.urls]
-
- start = time.time() # 记录开始时间
- # 并发执行所有协程任务,并等待完成
- loop.run_until_complete(asyncio.wait(tasks))
- end = time.time() # 记录结束时间
-
- # 打印执行时间
- print("use time seconds: ", end - start)
参考原文链接:https://blog.csdn.net/qq_39445165/article/details/124674435
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。