前言:
在Python中,协程(coroutines)是利用生成器(generator)的特性,来实现并发编程的一种方式。从Python 3.5开始,通过引入async和await关键字,Python对异步IO提供了更原生的支持,使得协程成为了实现异步编程的首选方式。协程提供了比线程更轻量级的并发,它们在单线程内执行,但在等待IO操作(如网络请求、数据库查询等)的同时,可以让出控制权,这样CPU就可以去做其他的计算任务,提高了程序的执行效率和响应速度。
1. 协程的基本概念
异步IO (asyncio模块): Python的标准库asyncio是用来编写单线程并发代码使用的,它使用了事件循环来管理异步任务,通过async def声明协程函数,使用await来挂起对耗时操作的等待。
async: 声明一个协程函数,函数内可以使用await。
await: 在协程函数中用于等待另一个协程完成并获取其结果,同时让出控制权,使得其他协程可以运行。
事件循环(Event Loop): 程序的入口点,用于调度和管理所有的协程。
2. 协程的使用场景
协程可以用于一些需要频繁切换的操作,例如I/O操作、网络通信、GUI应用等。由于协程不需要线程切换和进程切换的开销,因此在这些场景中可以更高效地利用计算机资源。同时,协程还可以避免由于多线程和多进程带来的线程安全问题,使得代码更加简洁易读。
3. 协程的示例:
#!/usr/bin/env python
# coding=utf-8
# Author: Summer
# Time: 2024.04.1
import asyncio
async def main():
print('Hello')
await asyncio.sleep(1)
print('world')
asyncio.run(main())
# 输出
# Hello
# world
这个简单的例子展示了一个异步协程的基本结构。main函数是一个协程,因为它是用async关键字定义的。在main协程中,await asyncio.sleep(1)这行代码让出了控制权,使得事件循环可以在这里挂起协程,去执行其他任务,在1秒后再回来继续执行。
再来举一个稍微复杂一点的例子,更好的说明协程的作用
#!/usr/bin/env python
# coding=utf-8
# Author: Summer
# Time: 2024.04.1
import asyncio
import aiohttp
import sys
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
async def save_content_to_file(url, session, file_name):
async with session.get(url) as response:
content = await response.text()
with open(file_name, 'w', encoding='utf-8') as file:
file.write(content)
print(f"Content from {url} has been saved to {file_name}")
async def main(urls):
async with aiohttp.ClientSession() as session:
tasks = []
for i, url in enumerate(urls):
file_name = f'website_{i}.txt'
task = asyncio.create_task(save_content_to_file(url, session, file_name))
tasks.append(task)
await asyncio.gather(*tasks)
# 准备一组URLs用于下载
urls = [
'http://www.python.org',
'https://www.baidu.com',
'https://www.bing.com',
# 更多URLs...
]
# 使用asyncio运行main协程及其所有子协程
asyncio.run(main(urls))
在上述代码中:
ave_content_to_file
这个协程负责获取特定URL的内容,并将其保存到本地文件中。具体步骤包括:
使用async with语法创建出一个异步上下文管理器,以session.get(url)的形式发送GET请求。这个操作是异步的,会挂起当前协程直到请求完成并返回响应对象。
通过await response.text()异步获取响应的文本内容。这会等待网络IO操作的完成,当内容被成功读取时,协程会继续执行。
通过普通的同步I/O操作with open(…) as file打开一个文件,文件名由file_name参数指定。
这个协程单独负责处理一个URL的下载和保存逻辑。
main
这个协程是程序的主入口。在这个协程中,执行了以下步骤:
创建一个共享的aiohttp.ClientSession(),在这个会话中,可以发送多个HTTP请求。整个会话使用async with包裹,确保会话在所有请求完成后被正确关闭。
初始化一个空的任务列表tasks。这是用来存放所有将要执行的save_content_to_file任务。
遍历给定的urls列表。对于列表中的每个URL,指定一个输出文件名file_name(按照website_{i}.txt的格式),创建一个save_content_to_file任务,并将这个任务添加到tasks列表中。
使用asyncio.gather(tasks)并发执行所有协程任务。asyncio.gather接收一个可迭代的协程列表,然后并发地运行它们,等待所有协程都运行完成。这里,星号操作符是将列表解包为函数的参数。
总结起来,main协程负责协调整个下载过程,为每个URL的下载创建异步任务,并监控它们直到全部完成。而save_content_to_file协程则关注于单个文件的下载和保存逻辑,将每个URL的内容保存到一个独立的文件中。
下面代码中的作用
调整事件循环策略(特别针对Windows)
如果你在Windows上遇到这个问题,并且确定问题是由Python 3.8及以后版本引入ProactorEventLoop导致的,你可以尝试改变事件循环的实现,切回到Python 3.7使用的SelectorEventLoop。在程序的开始执行:
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
由于我的python版本是python3.8.2 比较老的版本,在3.8的版本之后我的windows系统上运行协程函数会报这个错误,
这是由于RuntimeError: Event loop is closed错误通常发生在异步程序的事件循环被不恰当地关闭或处理了之后没有适当地处理清理动作,这个问题特别常见于Windows系统上使用Python的asyncio库时。这是由于从Python 3.8开始,在Windows上,默认的事件循环被改变为ProactorEventLoop。ProactorEventLoop对于某些类型的异步I/O操作更高效,但也可能导致一些兼容性问题。
4. 协程、多线程、多进程的区别:
想了解多线程和多进程的也可以看我之前的文章:
https://blog.csdn.net/weixin_41238626/article/details/137191039
多进程
内存隔离:每个进程有自己独立的内存空间,进程间通信需要特殊的IPC机制,如管道、信号、共享内存等。
开销较大:进程的创建、销毁和切换比线程和协程都有较大的开销。
稳定性高:一个进程崩溃不会影响其他进程。
适用场景:适合于CPU密集型计算,可以利用多核CPU的优势。
多线程
共享内存:线程运行在同一个进程内,共享相同的内存空间,线程间通信相对容易。
开销适中:线程的创建、销毁和切换的开销小于进程但大于协程。
稳定性低:一个线程崩溃可能会影响同进程内的其他线程,甚至整个进程。
GIL(Global Interpreter Lock,全局解释器锁):在CPython解释器中,由于GIL的存在,同一时间只能有一个线程执行Python字节码,因此多线程在CPU密集型任务中不一定有效。
适用场景:适合I/O密集型任务,如文件读写、网络通信等,可以在等待I/O操作完成时进行其他线程的任务处理。
协程
轻量级:协程是用户态的线程,协程的切换由程序自己控制,没有内核态的介入,因此开销极小。
单线程内并发:协程在单个线程内实现并发,通过任务切换达到并发的效果,不存在线程切换的开销。
协作式调度:协程是协作式的,意味着一个协程主动挂起(yield)之后,另一个协程才能运行。
适用场景:适合高并发的I/O密集型任务,如网络请求、数据库查询等。
对比总结
开销:协程 < 多线程 < 多进程。协程没有线程上下文切换的开销,更适合执行大量的I/O密集型任务。
并发性:在I/O密集型应用中,协程可以实现高并发,而在CPU密集型任务中多进程能够更好地利用多核处理器。
内存和安全:多进程最安全(由于内存隔离),多线程次之,协程在单个线程内管理多个任务可能需要更细致的错误处理。
易用性:协程通常需要特定的语法支持(async/await),而多线程和多进程的概念相对容易理解。
不同的并发模型各有优缺点,适用的场景也不同。在实际的开发工作中,可能会根据任务的特性综合考虑使用不同的并发模型。