Python-协程

目录

  • 一、迭代器与生成器
    • 1. 可迭代对象 Iterable
      • 1)定义
      • 2)判断可否为迭代对象
    • 2. 迭代器 Iterator
      • 1)定义
      • 2)判断是否为迭代器
      • 3)实现迭代器
      • 4)for 循环的本质
      • 5)利用迭代器实现斐波那契数列
    • 3. 生成器 generator
      • 1)() 创建生成器
      • 2)yield 创建生成器
      • 3)send() 函数唤醒(了解)
    • 4. 可迭代对象、迭代器、生成器对比
  • 二、协程 Coroutine
    • 1. 简单实现协程
    • 2. greenlet 模块
      • 1)安装 pip
      • 2)Windows 下安装 greenlet
      • 3)Python 代码示例
    • 3. gevent 模块
      • 1)Windows 下安装 gevent
      • 2)Python 代码示例
      • 3)给程序打补丁
    • 4. asyncio 模块
      • 1)Python 3.12 版本的 asyncio
      • 2)Python 代码示例
    • 5. 三种模块对比
      • 1)gevent 与 asyncio 示例代码对比
      • 2)出现 urllib3.exceptions.ProtocolError 错误
  • 三、使用协程的实例
    • 1. 使用协程爬取网页
      • 1)Python 代码实现
      • 2)出现 MonkeyPatchWarning 警告
    • 2. 下载多张图片
  • 四、进程、线程、协程对比


一、迭代器与生成器

下图是列表 / 元组 / 字典(list / tuple / dict)、容器(container)、可迭代对象(iterable)、迭代器(iterator)、生成器(generator)等之间的关系。

图片来源:【Python迭代器和生成器详解】

Python-协程_第1张图片

参考文章:
【python中生成器与迭代器到底有什么区别?一文带你彻底搞清楚】
【Python3 迭代器与生成器 | 菜鸟教程】
【Python生成器和迭代器的区别】

1. 可迭代对象 Iterable

1)定义

在 Python 中,可迭代对象(Iterable)是指可以一次返回其成员的对象,这意味着这些对象可以通过 for…in… 循环进行遍历。所有的序列类型,如列表(list)、字符串(str)和元组(tuple),以及一些非序列类型,如字典(dict)和文件对象,都是可迭代的。此外,任何定义了 _iter_() 方法的自定义对象也可以成为可迭代对象。

2)判断可否为迭代对象

如何判断一个对象是否可以迭代?传入 collections.abc.Iterable 并利用 isinstance() 函数,该函数用于判断一个对象是否是一个已知的类型,类似于 type() 。

isinstance() 与 type() 区别为:

  • type() 不会认为子类是一种父类类型,不考虑继承关系。

  • isinstance() 会认为子类是一种父类类型,考虑继承关系。

如果要判断两个类型是否相同推荐使用 isinstance() 函数。

Python 代码示例:

from collections.abc import Iterable
  
class MyList(object):  
    def __init__(self):  
        self.data = []  
  
    def __iter__(self):  
        pass  
  
if __name__ == '__main__':
	print(isinstance([], Iterable))  # True
	print(isinstance({}, Iterable))  # True
	print(isinstance('abc', Iterable))  # True
	print(isinstance(100, Iterable))  # False
    my_list = MyList()  
    print(isinstance(my_list, Iterable))  # True

2. 迭代器 Iterator

1)定义

可以通过 iter() 函数获取可迭代对象的迭代器(Iterator),每个迭代器都是可迭代对象。通过重复调用迭代器的 next() 方法(或将其传递给内置函数 next() )可以获取下一条数据。当没有更多数据可用时,会引发 StopIteration 异常,此时,迭代器对象被耗尽,任何进一步调用其 next() 方法都将再次引发 StopIteration 异常。

2)判断是否为迭代器

Python 代码示例:

from collections.abc import Iterable  
from collections.abc import Iterator  
  
print(isinstance([], Iterable))  # True  
print(isinstance([], Iterator))  # False  
print(isinstance(iter([]), Iterable))  # True  
print(isinstance('abc', Iterator))  # False  
print(isinstance(iter('abc'), Iterator))  # True

3)实现迭代器

Python 代码示例:

from collections.abc import Iterable
from collections.abc import Iterator


class MyIterator(object):
    def __init__(self, my_list):
        self.my_list = my_list
        self.current_index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.current_index < len(self.my_list.items):
            item = self.my_list.items[self.current_index]
            self.current_index += 1
            return item
        else:
            raise StopIteration


class MyList(object):
    def __init__(self):
        self.items = []

    def add(self, item):
        self.items.append(item)

    def __iter__(self):
        return MyIterator(self)


if __name__ == '__main__':
    mylist = MyList()
    print(f'是否为迭代对象:{isinstance(mylist, Iterable)}')  # True
    print(f'是否为迭代器:{isinstance(mylist, Iterator)}')  # False
    mylist.add(1)
    mylist.add(2)
    mylist.add(3)
    for i in mylist:
        print(i, end=' ')  # 1 2 3
    print()
    # ----------------------------------------
    my_iterator = MyIterator(mylist)
    print(f'是否为迭代对象:{isinstance(my_iterator, Iterable)}')  # True
    print(f'是否为迭代器:{isinstance(my_iterator, Iterator)}')  # True
    for j in my_iterator:
        print(j, end=' ')  # 1 2 3

4)for 循环的本质

for item in Iterable 循环的本质就是先通过 iter() 函数获取可迭代对象 Iterable 的迭代器 Iterator ,然后对获取到的迭代器不断调用 next() 方法来依次获取对象中的每一个数据并将其赋值给 item ,当遇到 StopIteration 的异常后循环结束。

总结:

  • 可迭代对象(Iterable)是实现了 _iter_() 方法的对象;迭代器(Iterator)是实现了 _iter_() 方法和 _next()_ 方法的对象。

  • 通过调用 iter() 方法可以获得一个迭代器(Iterator)。

  • for…in… 的迭代实际是将可迭代对象转换成迭代器,再重复调用 next() 方法实现。

5)利用迭代器实现斐波那契数列

Python 代码示例:

class FibIterator(object):  # 斐波那契数列迭代器
    def __init__(self, n):
        # n 指明生成数列的前 n 个数
        self.n = n
        # current 用来保存当前生成到数列中的第几个数了
        self.current = 0
        # num1 用来保存前前一个数,初始值为数列中的第一个数 0
        self.num1 = 0
        # num2 用来保存前一个数,初始值为数列中的第二个数 1
        self.num2 = 1

    def __next__(self):  # 调用 next() 函数获取下一个数
        if self.current < self.n:
            result = self.num1
            self.num1, self.num2 = self.num2, self.num1 + self.num2
            self.current += 1
            return result
        else:
            raise StopIteration

    def __iter__(self):  # 迭代器的 __iter__ 返回自身即可
        return self


if __name__ == '__main__':
    # ---------- for 循环接收可迭代对象 ----------
    fib = FibIterator(10)
    for num in fib:
        print(num, end=" ")
    print()  # 0 1 1 2 3 5 8 13 21 34
    # ---------- next() 函数接收可迭代对象 ----------
    f_n = FibIterator(10)
    while True:
        try:
            n = next(f_n)
            print(n, end=" ")
        except StopIteration:
            break
    print()  # 0 1 1 2 3 5 8 13 21 34
    # ---------- list() tuple() 接收可迭代对象 ----------
    li = list(FibIterator(8))
    print(li)  # [0, 1, 1, 2, 3, 5, 8, 13]
    tp = tuple(FibIterator(8))
    print(tp)  # (0, 1, 1, 2, 3, 5, 8, 13)

3. 生成器 generator

生成器(generator)是一种特殊的迭代器,它是一个包含 yield 表达式的函数,用于在 for 循环中产生一系列值,或者可以一次一个地使用 next() 函数检索。

生成器通常指的是生成器函数,但在某些上下文中也可能指生成器迭代器。

1)() 创建生成器

创建列表生成式和生成器的区别仅在于最外层的 [ ] 和 ( ) ,可以按照迭代器的方法打印生成器中的每个元素,即可以通过 next() 函数、for 循环、list() 函数、tuple() 函数等方法。

Python 代码示例:

A = (x*2 for x in range(10))
print(type(A))  # 
while True:
    try:
        a = next(A)
        print(a, end=' ')  # 0 2 4 6 8 10 12 14 16 18
    except StopIteration:
        break

2)yield 创建生成器

只要在 def 中有 yield 关键字就称为生成器。yield 关键字的作用是返回一个值,并暂停函数的执行,下一次调用函数时从上一次暂停的地方继续执行。

Python 代码示例:

def fib(n):
    num1, num2 = 0, 1
    current = 0
    while current < n:
        num = num1
        num1, num2 = num2, num1 + num2
        current += 1
        yield num
    return 'done'


if __name__ == '__main__':
    F = fib(10)
    while True:
        try:
            a = next(F)
            print(a, end=' ')  # 0 1 1 2 3 5 8 13 21 34 done
        except StopIteration as e:
            print(e.value)
            break
  • 使用了 yield 关键字的函数不再是函数,而是生成器。

  • yield 关键字有两点作用:

    • 保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起。
    • 将 yield 关键字后面表达式的值作为返回值返回,即可以理解为等价于 return 的作用。
  • 使用 next() 函数让生成器从断点处继续执行,即唤醒生成器(函数)

  • Python3 中的生成器可以使用 return 返回最终运行的返回值。

3)send() 函数唤醒(了解)

除了可以使用 next() 函数唤醒生成器并继续执行外,还可以使用 send() 函数来唤醒执行。使用 send() 函数的一个好处是:可以在唤醒的同时向断点处传入一个附加数据,即 next() 等价于 send(None) 。

Python 代码示例:

def add_one(n):
    i = 0
    while i < n:
        result = yield i
        print(result)
        # yield i
        i += 1

if __name__ == '__main__':
    a = add_one(5)
    print(next(a))  # 0
    print('-' * 10)
    print(a.send('hello'))  # hello 1
    print('-' * 10)
    print(next(a))  # None 2 ,等价于 print(a.send(None))

4. 可迭代对象、迭代器、生成器对比

特性 可迭代对象 Iterable 迭代器 Iterator 生成器 Generator
定义 实现了 __iter__() 方法,能够返回一个迭代器 实现了 __iter__()__next__() 方法的对象 特殊的迭代器,由生成器函数或生成器表达式创建
作用 提供一个接口可用于遍历元素 用于遍历元素,生成序列中的下一个元素 生成一个值序列的迭代器,支持惰性计算
方法 __iter__() 返回迭代器 __iter__() 返回自身,__next__() 返回下一个元素 继承自迭代器,实现 __iter__()__next__()
使用方式 可直接用于 for 循环 通常不直接用for,需手动调用next()获取值 可直接用于 for 循环或手动调用 next()
关系 可生成迭代器,迭代器是它的 “产物” 是可迭代对象的具体遍历器 是一种特殊的迭代器,由生成器函数生成
是否有状态 通常无状态,持有集合或容器 有状态,跟踪当前元素的位置 有状态,且可以在暂停处保存上下文和变量状态
生成方式 任何实现了 __iter__() 的对象,如 list、tuple 由可迭代对象的 __iter__() 创建 使用 yield 关键字的函数
优点 简单,可遍历多种容器对象 支持遍历所有元素,避免一次性加载全部数据 支持惰性生成,节省内存,生成复杂序列更灵活
示例 list、str、dict list 的 iter(list) 返回的对象 def gen(): yield 1 创建的生成器对象

二、协程 Coroutine

  • 协程是一种轻量级的并发执行方式,允许程序在执行过程中暂停并恢复,而无需操作系统介入。

  • 相比于进程和线程,协程避免了内核态和用户态的切换开销,提高了执行效率。

  • 协程在遇到 I/O 阻塞时不会阻塞整个进程,且不需要加锁机制,简化了同步问题。

  • 协程是一种用户级的轻量级线程,拥有自己的寄存器上下文和栈,调度切换时保存和恢复寄存器上下文和栈。

  • 协程允许执行被挂起与被恢复。

  • 在单线程里,多个函数可以并发地执行,这就是协程。

在多 CPU 核数下多进程、多线程可能是并行执行的,但是协程只存在在一个线程中, 因此协程切换时任务资源很小,效率高,且只能是并发执行。

1. 简单实现协程

Python 代码示例:

import time


def work1():
    while True:
        print("----work1---")
        yield
        time.sleep(0.5)


def work2():
    while True:
        print("----work2---")
        yield
        time.sleep(0.5)


def main():
    w1 = work1()
    w2 = work2()
    while True:
        next(w1)
        next(w2)


if __name__ == "__main__":
    main()

运行结果:

----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
… …

2. greenlet 模块

greenlet 是一个底层工具,适合需要手动管理协程切换的场景。

1)安装 pip

  • 下载 pip 安装包,并保存在指定路径下,例如 “D:\” ,下载好后进行解压。

pip 安装包网址:【pip 25.1.1】

  • 在解压文件的目录下进入命令提示符 cmd ,输入 python setup.py install 命令进行安装,安装成功后重启电脑。

  • 在命令提示符 cmd 上输入 pip list 命令,测试 pip 是否安装成功。

  • 快捷键 Win + R ,输入 %APPDATA% 并回车,进入 “C:\Users\[用户名]\AppData\Roaming” 目录下,在 pip 文件夹下(没有则创建)新建 pip.ini 文件。

  • 以记事本方式打开 pip.ini 文件,编辑以下内容并保存:

[global]
timeout = 6000
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
trusted-host = pypi.tuna.tsinghua.edu.cn
  • “设置” → “系统” → “系统信息” → “高级系统设置” → “高级” → “环境变量(N)…” ,将 C:\Users\[用户名]\AppData\Roaming\pip\pip.ini 新建至 PATH 路径下。

注:升级 pip 的命令为 python -m pip install --upgrade pip

2)Windows 下安装 greenlet

  • 打开 cmd ,输入 pip install greenlet 命令并回车。

  • 输入 pip list|grep greenlet 命令验证 greenlet 是否安装成功。

greenlet 模块的常用方法和属性:

方法 介绍
switch() 切换到指定的 greenlet 并执行其代码
getcurrent() 返回当前正在运行的 greenlet 对象
属性 介绍
parent 表示当前 greenlet 的父级 greenlet(通常是创建它的 greenlet)
dead 检查 greenlet 是否已经完成执行

3)Python 代码示例

import time
from greenlet import greenlet


def work1():
    while True:
        print("----work1---")
        gr2.switch()
        time.sleep(0.5)


def work2():
    while True:
        print("----work2---")
        gr1.switch()
        time.sleep(0.5)



gr1 = greenlet(work1)
gr2 = greenlet(work2)
# 切换到 gr1 中运行
gr1.switch()

运行结果:

----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
… …

3. gevent 模块

gevent 是一个基于 greenlet 实现的网络库,通过 greenlet 实现协程。

gevent 模块的原理是:当 greenlet 遇到 I/O(指 input output 输入输出,比如网络、文件操作等)操作时,比如访问网络阻塞,就自动切换到其他的协程,等到 I/O 操作完成,再在适当的时候切换回来继续执行。

由于 I/O 操作非常耗时,经常使程序处于等待状态,有了 gevent 模块自动切换协程,就保证总有协程在运行,而不是在等待 I/O 。

1)Windows 下安装 gevent

  • 打开 cmd ,输入 pip install gevent 命令并回车。

  • 输入 pip list|grep gevent 命令验证 gevent 是否安装成功。

gevent 模块的常用方法:

方法 介绍
gevent.spawn() 创建一个普通的 greenlet 对象并切换
gevent.spawn_later(seconds=3) 延时创建一个普通的 greenlet 对象并切换
gevent.spawn_raw() 创建的协程对象属于一个组
gevent.getcurrent() 返回当前正在执行的 greenlet
gevent.joinall(jobs) 将协程任务添加到事件循环,接收一个任务列表
gevent.wait() 可以替代 join 函数等待循环结束,也可以传入协程对象列表
gevent.kill() 杀死一个协程
gevent.killall() 杀死一个协程列表里的所有协程
monkey.patch_all() 自动将 python 的一些标准模块替换成 gevent 框架

2)Python 代码示例

import gevent

def run(n):
    for i in range(n):
        print(f'协程 : {gevent.getcurrent()} 执行任务 : {i}')
        # 用来模拟一个耗时操作,注意不是 time 模块中的 sleep
        gevent.sleep(1)

gr1 = gevent.spawn(run, 3)
gr2 = gevent.spawn(run, 3)
gr3 = gevent.spawn(run, 3)
gr1.join()
gr2.join()
gr3.join()

运行结果:

协程 : <Greenlet at 0x19a46eba200: run(3)> 执行任务 : 0
协程 : <Greenlet at 0x19a7250a3e0: run(3)> 执行任务 : 0
协程 : <Greenlet at 0x19a7250a2a0: run(3)> 执行任务 : 0
协程 : <Greenlet at 0x19a46eba200: run(3)> 执行任务 : 1
协程 : <Greenlet at 0x19a7250a3e0: run(3)> 执行任务 : 1
协程 : <Greenlet at 0x19a7250a2a0: run(3)> 执行任务 : 1
协程 : <Greenlet at 0x19a46eba200: run(3)> 执行任务 : 2
协程 : <Greenlet at 0x19a7250a3e0: run(3)> 执行任务 : 2
协程 : <Greenlet at 0x19a7250a2a0: run(3)> 执行任务 : 2

3)给程序打补丁

猴子补丁的作用是:在程序运行时动态替换某些功能,通常在程序启动时进行。举个例子,如果一个游戏服务器的代码里有多处写了 import json ,后来发现使用 ujson 这个库比自带的 json 快好多倍,那么需要去每个文件中把 import json 改成 import ujson as json 吗?其实不用,只要在程序启动时用猴子补丁替换一次即可,这样整个进程里所有的 json 调用都会变成用 ujson 。

import json  
import ujson  

def monkey_patch_json():  
    json.__name__ = 'ujson'  
    json.dumps = ujson.dumps  
    json.loads = ujson.loads  

monkey_patch_json()

再比如用协程访问多个网站的情况,因为网络 I/O 操作很耗时,程序会经常处于等待状态。用 gevent 时,只要在启动时执行 gevent.monkey.patch_all() ,它会自动帮我们处理阻塞,遇到等待时自动切换协程,这样程序运行就更高效了。

Python 代码示例:

from gevent import monkey
import gevent
import random
import time

# 有耗时操作时需要,将程序中用到的耗时操作的代码,换为 gevent 中自己实现的模块
monkey.patch_all()


def coroutine_work(coroutine_name):
    for i in range(3):
        print(f'协程 : {coroutine_name} 执行任务 : {i}')
        time.sleep(random.random())


gevent.joinall([gevent.spawn(coroutine_work, "work1"), gevent.spawn(coroutine_work, "work2")])

运行结果:

协程 : work1 执行任务 : 0
协程 : work2 执行任务 : 0
协程 : work2 执行任务 : 1
协程 : work1 执行任务 : 1
协程 : work2 执行任务 : 2
协程 : work1 执行任务 : 2

4. asyncio 模块

1)Python 3.12 版本的 asyncio

在 Python 3.12 中,asyncio 包的性能得到了多项改进,其中一些基准测试显示有 75% 的提速。这些改进包括:

  • 改进的事件循环性能:事件循环的性能优化使得任务调度和 I/O 操作更加高效。

  • 优化的任务切换:减少了上下文切换的开销,提升了任务执行效率。

  • 更快的 I/O 操作:对底层 I/O 操作进行了优化,提升了整体吞吐量。

  • 增强的 asyncio.run :在处理大量并发任务时,asyncio.run 的性能得到了改进。

这些改进使得使用 asyncio 进行异步编程在 Python 3.12 中更加高效,适合处理大量并发的 I/O 绑定任务。

2)Python 代码示例

# 示例:使用 asyncio 的异步 I/O 操作
import asyncio
import random


async def fetch_data(delay, name):
    """
    async def fetch_data(delay, name):定义一个异步函数,该函数模拟一个耗时操作。
    await asyncio.sleep(delay):模拟延迟操作,使当前协程暂停 delay 秒,而不阻塞事件循环。这使得其他协程可以在这段时间内运行。
    print 和 return:分别输出任务开始和结束的信息,并返回任务的结果。
    """
    print(f'Starting fetch for {name} with a delay of {delay} seconds...')
    await asyncio.sleep(delay)
    print(f'Finished fetch for {name}')
    return f'Data from {name}'


async def main():
    # 定义多个异步任务
    tasks = []
    for i in range(1, 4):
        tasks.append(fetch_data(random.randint(1, 5), f'Task{i}'))
    # 并发执行所有任务,并等待它们完成,gather()方法返回一个列表
    results = await asyncio.gather(*tasks)
    # 打印结果
    print(results)
    # 在 Python 3.12 中直接运行异步主函数
    # Python 3.12 asyncio 包的性能获得了多项改进,一些基准测试显示有 75% 的提速


if __name__ == '__main__':
    asyncio.run(main())  # 3.12 及以上版本

运行结果:

Starting fetch for Task1 with a delay of 1 seconds...
Starting fetch for Task2 with a delay of 4 seconds...
Starting fetch for Task3 with a delay of 3 seconds...
Finished fetch for Task1
Finished fetch for Task3
Finished fetch for Task2
['Data from Task1', 'Data from Task2', 'Data from Task3']

5. 三种模块对比

  • greenlet :轻量,适合做底层协程调度,但开发复杂,适合高级用户用来自定义协程模型。

asyncio 和 gevent 都是用于异步编程的库,但它们在设计和实现上有一些显著的不同,这影响了它们在不同场景中的效率和适用性。

  • gevent :基于 greenlet ,增强了网络 I/O 的支持和自动协程调度,适合网络高并发场景,封装成熟。

  • asyncio :Python 标准异步框架,语法现代且有广泛生态支持,适合新项目和未来发展,学习曲线相对较陡。

特性 greenlet gevent asyncio
定义和定位 轻量级协程库,实现协程的切换(微线程) 基于 greenlet 的协程库,提供事件驱动网络编程,实现协程自动切换 Python 标准库原生异步框架,基于事件循环和协程编程
核心实现原理 协程切换,由开发者手动切换(协程的上下文切换) 事件循环 + 协程自动切换,内置大量网络 I/O 非阻塞操作封装 事件循环,任务调度,基于 async/await 语法
编程模型 手动管理切换 自动调度,写法简单,类似同步编程 异步 / await 语法,原生支持异步函数
I/O 模型 不提供原生非阻塞 I/O ,需要配合其他库实现 内置非阻塞网络和 I/O 库,自动切换协程 事件驱动非阻塞 I/O ,依赖异步 I/O 库
生态与依赖 体积小,无额外依赖 依赖 greenlet ,生态丰富,广泛用于网络服务器 Python 标准库,依赖底层异步支持(如 Proactor、Selector 事件循环)
易用性 低,需手动切换,使用门槛较高 高,封装良好,接近同步模型,易读易维护 中等,需要理解 async / await 及事件循环机制
性能 切换开销极低,且切换灵活 较高效,适合大量并发 I/O 任务,切换快速 性能良好,标准库支持,适合大规模异步任务
主应用场景 需要细粒度协程调度的场景 网络并发服务器,如爬虫、聊天服务器等 Web 服务、异步数据库操作、异步 HTTP 客户端等
优点 轻量、无依赖、切换快速,适合底层协程库 生态丰富,支持多种网络协议库,使用方便,支持猴子补丁协程化第三方库 标准库支持,语法现代化,方便集成,适合异步任务和现代 Python async 生态
缺点 编程模型复杂,需手动管理切换,缺乏高级封装 依赖 greenlet ,猴子补丁可能带来兼容问题 学习曲线较陡,运行时调试较复杂,部分同步库不兼容异步环境

性能对比:

情况 greenlet gevent asyncio
上下文切换开销 最小(极快) 较小 较小
并发处理 I/O 低(需手动处理) 高(自动协程切换,非阻塞 I/O) 高(事件循环和原生异步 I/O)
CPU 密集任务 无特殊优势 无特殊优势 无特殊优势
兼容性 高(裸协程切换层) 有时会遇到第三方库兼容问题(猴子补丁) 与 Python 生态兼容最好

结论:

  • 如果需要与其他现代 Python 异步库和框架进行集成,或者希望使用标准库并获得跨平台支持,asyncio 是一个更好的选择。

  • 如果需要处理大量小型 I/O 任务,并且希望简化代码编写,gevent 可能会更高效。

1)gevent 与 asyncio 示例代码对比

# asyncio 示例

import asyncio
import aiohttp  # pip install aiohttp

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, 'http://baidu.com') for _ in range(4)]
        results = await asyncio.gather(*tasks)
        for result in results:
            print(result)

if __name__ == '__main__':
    asyncio.run(main())
# gevent 示例

import gevent
from gevent import monkey

monkey.patch_all()

import requests
import time


def fetch(url):
    response = requests.get(url)
    time.sleep(0.5)
    return response.text


def main():
    urls = ['http://baidu.com' for _ in range(4)]
    jobs = [gevent.spawn(fetch, url) for url in urls]
    gevent.joinall(jobs)
    for job in jobs:
        print(job.value)


if __name__ == '__main__':
    main()

运行结果:

Python-协程_第2张图片

2)出现 urllib3.exceptions.ProtocolError 错误

问题:在运行 gevent 模块的 Python 代码示例时,出现 “urllib3.exceptions.ProtocolError: (‘Connection aborted.’, ConnectionResetError(10054, ‘远程主机强迫关闭了一个现有的连接。’, None, 10054, None))” 的错误怎么办?

原因:没有 sleep 几秒,从而在短时间内对网站大量使用 requests 操作,导致网站认定是攻击行为,因此抛出异常。

解决方法:在头部引入 import time ,并在调用 requests 的循环内,加入方法 time.sleep(0.5) 即可解决。

三、使用协程的实例

1. 使用协程爬取网页

1)Python 代码实现

from gevent import monkey
import gevent

# 有耗时操作时需要
monkey.patch_all()

import requests  # cmd 下 pip install requests
import time


def my_download(url):
    print('GET: <%s> .' % url)
    response = requests.get(url)
    data = response.text
    print('%d bytes received from <%s> .' % (len(data), url))


# 使用协程
start = time.time()
gevent.joinall([
    gevent.spawn(my_download, 'http://www.baidu.com/'),
    gevent.spawn(my_download, 'http://www.163.com/'),
    gevent.spawn(my_download, 'http://www.runoob.com/'),
    gevent.spawn(my_download, 'http://www.51cto.com/'),
    gevent.spawn(my_download, 'http://www.cnblogs.com/')
])
end = time.time()
print('The total time spent using coroutines: (%f) .' % (end - start))

print('-' * 50)

# 不使用协程
start = time.time()
my_download('http://www.baidu.com/')
my_download('http://www.163.com/')
my_download('http://www.runoob.com/')
my_download('http://www.51cto.com/')
my_download('http://www.cnblogs.com/')
end = time.time()
print('The total time spent without using coroutines: (%f) .' % (end - start))

运行结果展示:

Python-协程_第3张图片

由上图可以看出:使用协程时收到数据的先后顺序不一定与发送顺序相同,这体现出了异步,即不确定什么时候会收到数据。

2)出现 MonkeyPatchWarning 警告

运行时出现 “MonkeyPatchWarning: Monkey-patching ssl after ssl has already been imported may lead to errors, including RecursionError on Python 3.6. It may also silently lead to incorrect behaviour on Python 3.7. Please monkey-patch earlier. See …… Modules that had direct imports (NOT patched): ……” 的警告怎么办?

解决方法:将下面三行代码写在所有引入语句的最前面,即可解决。

import gevent
from gevent import monkey

monkey.patch_all()

2. 下载多张图片

from gevent import monkey
import gevent

# 有耗时操作时需要
monkey.patch_all()

import requests  # cmd 下 pip install requests


def my_download(file_name, url):
    print('GET: <%s> .' % url)
    response = requests.get(url)
    data = response.content
    with open(file_name, "wb") as f:
        f.write(data)
    print('%d bytes received from <%s> .' % (len(data), url))


gevent.joinall([
    gevent.spawn(my_download, "picture1.jpg",
                 'http://qzs.qq.com/qzone/v6/v6_config/upload/7a082c0dde36eac2205a088397aaf295.jpg'),
    gevent.spawn(my_download, "picture2.jpg",
                 'https://picb4.photophoto.cn/25/832/25832894_1.jpg'),
    gevent.spawn(my_download, "picture3.jpg",
                 'https://cdn.pixabay.com/photo/2025/05/30/03/00/border-collie-9630551_1280.jpg')
])

运行结果展示:

Python-协程_第4张图片

四、进程、线程、协程对比

特性 进程 Process 线程 Thread 协程 Coroutine
定义 操作系统管理的资源分离的执行单元 进程内的轻量级执行单元,共享进程资源 程序内部调度的轻量级 “用户级线程”
资源隔离 独立内存空间,资源完全隔离 共享进程内存资源 共享进程内存,由程序调度
创建开销 大,涉及系统调用及资源分配 较小,系统线程上下文切换开销中等 极小,协程切换在用户态完成
切换开销 高,操作系统切换上下文 中,操作系统线程切换 低,协程切换几乎无系统调用
并发类型 真并行(多核),适合 CPU 密集型 伪并行(受 GIL 限制,CPython 中只有一线程执行 Python 字节码) 单线程内的并发,非抢占式切换
适用场景 CPU 密集型任务,进程间隔离高的任务 I/O 密集型、多线程任务但 CPU 利用有限 大量 I/O 密集且逻辑清晰的异步编程
编程复杂度 相对复杂,进程间通信(IPC)较麻烦 复杂,竞态条件和锁机制需注意 简单,代码结构清晰,顺序风格异步
线程安全 不存在线程安全问题,但需进程间通信同步 存在线程安全问题,需同步机制 无需锁,多数情况下是顺序执行
内存占用 多,独立内存空间 少,共享内存 极少,协程是函数级别调度
调试难度 较高 较低
Python GIL 影响 无影响,多个进程不共享 GIL 影响较大,不能利用多核 CPU 不存在 GIL 问题,单线程运行,但异步并发
操作系统依赖 强,依赖底层操作系统 依赖操作系统线程模型 纯用户态实现,无需操作系统支持
示例库 multiprocessing threading asyncio , greenlet , gevent

优缺点总结:

类型 优点 缺点
进程 真正多核并行,资源隔离好,稳定性高 启动慢、切换开销大,进程间通信复杂
线程 启动快,系统支持,适合 I/O 密集型 受 GIL 限制,线程安全复杂,可能出现死锁
协程 轻量级,切换快,无 GIL 限制,适合高并发 I/O 只能运行在单线程内,CPU 密集任务效果差,需要异步编程

适用选择建议:

  • CPU 密集型:优先选多进程,避免 GIL 瓶颈。

  • I/O 密集型:多线程或协程,协程效率更高且代码结构优雅。

  • 高并发网络服务:推荐使用协程(如 asynciogevent)。

  • 复杂数据隔离或安全要求高的场景:选择多进程。

你可能感兴趣的:(操作系统,Python,python,协程)