01.协程

https://www.cnblogs.com/xiaonq/p/7905347.html#i4

1.1 什么是协程

1.1.1 协程的特点

  • 协程是一种用户态的轻量级线程,由用户代码自行管理,而不是操作系统调度
  • 非抢占式调度,协程只有在显式 yield 或 await 时才会主动让出 CPU。
  • 单线程执行,避免了线程切换的锁竞争问题,适用于 IO 密集型任务。

1.1.2 协程的优点

  • 切换开销极低:协程切换仅涉及栈和寄存器的切换,不需要系统调用。
  • 无锁编程:协程在同一个线程中执行,不会有数据竞争问题,避免了线程同步的开销。
  • 适用于高并发 IO:如异步 Web 服务器(FastAPI、Node.js)、网络爬虫等。

1.2 协程优缺点

  • 协程缺点

    • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上
    • 协程如果阻塞掉,整个程序都阻塞
  • 协程最大的优点

    • 不仅是处理高并发(单线程下处理高并发)
    • 特别节省资源(协程本质是一个单线程,当然节省资源)
      • 500日活,用php写需要两百多态机器,但是golang只需要二十多太机器

1.3 协程遇到I/O切换,那活只谁干的?

  • 简单说法

    • 协程遇到I/O后自动切换,但是会保持一个socket连接,交给系统内核去处理工作
    • epoll()就工作内核中,他维护了一个链表,来存放所有的socket连接
    • 当内核处理完成后就会回调一个函数,以socket文件描述符为key,结果为value存放到字典中
    • 此时这个列表还是在内核中,需要将这个字典拷贝到用户空间(用户进程中)
  • 本质

    • 1.epoll()中内核则维护一个链表,epoll_wait直接检查链表是不是空就知道是否有文件描述符准备好了。
    • 2.在内核实现中epoll是根据每个sockfd上面的与设备驱动程序建立起来的回调函数实现的。
    • 3.某个sockfd上的事件发生时,与它对应的回调函数就会被调用,来把这个sockfd加入链表,其他处于“空闲的”状态的则不会。
    • 4.epoll上面链表中获取文件描述,这里使用内存映射(mmap)技术,避免了复制大量文件描述符带来的开销
    • 内存映射(mmap):内存映射文件,是由一个文件到一块内存的映射,将不必再对文件执行I/O操作

1.4 适用场景:

  • 高并发 IO 任务:(如异步 Web 服务器、爬虫)。
  • 事件驱动型编程:(如游戏引擎、GUI 事件循环)。
  • 异步数据库访问: (如 asyncpg、aiomysql)。

1.5 Python中协程的模块

  • greenlet:遇到I/O手动切换,是一个C模块
  • gevent:对greenlet封装,遇到I/O自动切换借助C语言库greenlet
  • asyncio:和gevent一样,也是实现协程的一个模块(python自己实现

02.进程,线程,协程爬取页面

  • 特点:
    • 1.进程:启用进程非常浪费资源
    • 2.线程:线程多,并且在阻塞过程中无法执行其他任务
    • 3.协程:gevent只用起一个线程,当请求发出去后gevent就不管,永远就只有一个线程工作,谁先回来先处理

2.1 for循环

  • 第四:性能最差
1
2
3
4
5
6
7
8
9
import requests
url_list = [
'https://www.baidu.com',
'http://dig.chouti.com/',
]

for url in url_list:
result = requests.get(url)
print(result.text)

2.2 进程池

  • 缺点:启用进程非常浪费资源

2.2.1 multiprocessing.Pool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# -*- coding: utf-8 -*-
import requests
from multiprocessing import Pool

def fetch_request(url):
result = requests.get(url)
print(result.text)

def call(arg):
print('-->exec done:',"测试进程池执行后回调功能")

url_list = [
'https://www.baidu.com',
'https://www.google.com/', #google页面会卡住,知道页面超时后这个进程才结束
'http://dig.chouti.com/', #chouti页面内容会直接返回,不会等待Google页面的返回
]

if __name__ == '__main__':
pool = Pool(10) # 创建线程池
for url in url_list:
#用法1 callback作用是指定只有当Foo运行结束后就执行callback调用的函数,父进程调用的callback函数
pool.apply_async(func=fetch_request, args=(url,),callback=call)
print('end')
pool.close() #关闭pool
pool.join() #进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。

2.2.2 ProcessPoolExecutor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
from concurrent.futures import ProcessPoolExecutor

def fetch_request(url):
result = requests.get(url)
print(result.text)

url_list = [
'https://www.baidu.com',
'https://www.google.com/', #google页面会卡住,知道页面超时后这个进程才结束
'http://dig.chouti.com/', #chouti页面内容会直接返回,不会等待Google页面的返回
]

if __name__ == '__main__':
pool = ProcessPoolExecutor(10) # 创建线程池
for url in url_list:
pool.submit(fetch_request,url) # 去线程池中获取一个进程,进程去执行fetch_request方法
pool.shutdown(wait=True)

2.3 线程池

  • 缺点: 创建一个新线程将消耗大量的计算资源,并且在阻塞过程中无法执行其他任务。
  • 例: 比如线程池中10个线程同时去10个url获取数据,当数据还没来时这些线程全部都在等待,不做事。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
from concurrent.futures import ThreadPoolExecutor

def fetch_request(url):
result = requests.get(url)
print(result.text)

url_list = [
'https://www.baidu.com',
'https://www.google.com/', #google页面会卡住,知道页面超时后这个进程才结束
'http://dig.chouti.com/', #chouti页面内容会直接返回,不会等待Google页面的返回
]

pool = ThreadPoolExecutor(10) # 创建一个线程池,最多开10个线程
for url in url_list:
pool.submit(fetch_request,url) # 去线程池中获取一个线程,线程去执行fetch_request方法

pool.shutdown(True) # 主线程自己关闭,让子线程自己拿任务执行

2.4 协程

  • 特点 :gevent只用起一个线程,当请求发出去后gevent就不管,永远就只有一个线程工作,谁先回来先处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import gevent
from gevent import monkey
monkey.patch_all(select=False) # 注意,这个导包顺序不要变
import requests

# 这些请求谁先回来就先处理谁
def fetch_async(method, url, req_kwargs):
response = requests.request(method=method, url=url, **req_kwargs)
print(response.url, response.content)

# ##### 发送请求 #####
gevent.joinall([
gevent.spawn(fetch_async, method='get', url='https://www.baidu.com/', req_kwargs={}),
gevent.spawn(fetch_async, method='get', url='https://www.google.com/', req_kwargs={}),
gevent.spawn(fetch_async, method='get', url='https://github.com/', req_kwargs={}),
])

3、结论

  • 如果任务是 CPU 密集型(如计算、AI 训练),建议使用多进程来充分利用多核 CPU。
  • 如果任务是 IO 密集型(如网络请求、数据库查询),协程是更高效的选择,避免线程切换的开销。
  • 如果任务需要多个并行执行单元共享数据(如 GUI、游戏开发),多线程是一个合适的方案,但要注意线程同步问题。
  • 在高并发场景下,可以使用 “多进程 + 协程” 结合的方式,如 Nginx、FastAPI、Node.js 采用的模式。

4. 进程、线程、协程对比总结

对比项 进程 线程 协程
调度方式 操作系统 操作系统 用户态(手动切换)
资源占用 高(独立内存) 低(共享进程内存) 极低(仅栈和寄存器)
通信方式 IPC(管道、消息队列) 共享变量(需加锁) 共享变量(无锁)
创建开销
上下文切换 较快 极快
是否并行 是(多进程可用多核 ) 取决于语言(Java 可以,Python 受 GIL 限制) 否(单线程内运行)
适用场景 计算密集型、多核并行 IO 密集型、需要共享数据 高并发

__END__