协程
协程又称为微线程, 协程是一种用户态的轻量级线程
协程拥有自己的寄存器和栈协程调度切换的时候, 将寄存器上下文和栈都保存到其他地方, 在切换回来的时候, 恢复到先前保存的寄存器上下文和栈, 因此: 协程能保留上一次调用状态, 每次过程重入时, 就相当于进入上一次调用的状态
协程的好处:
1. 无需线程上下文切换的开销(还是单线程)
2. 无需原子操作 (一个线程改一个变量, 改一个变量的过程就可以称为原子操作) 的锁定和同步的开销
3. 方便切换控制流, 简化编程模型
4. 高并发 + 高扩展 + 低成本: 一个 cpu 支持上万的协程都没有问题, 适合用于高并发处理
缺点:
1. 无法利用多核的资源, 协程本身是个单线程, 它不能同时将单个 cpu 的多核用上, 协程需要和进程配合才能运用到多 cpu 上(协程是跑在线程上的)
2. 进行阻塞操作时会阻塞掉整个程序: 如 io
使用 yield 实现协程的例子:
- import time
- import queue
- def consumer(name):
- print("---->start eating baozi......")
- while True:
- #yield 默认可以返回数据, 走到 yield 整个程序返回, yield 被唤醒的时候还可以接收数据
- #进入死循环碰到 yield 暂停, 被唤醒的时候才执行 print
- new_baozi = yield
- print("[%s] is eating baozi %s" %(name,new_baozi))
- #time.sleep(2)
- def producer():
- #__next__()调用消费者的 next,consumer 直接调用的话, 第一次不会执行, 会变成一个生成器
- #函数如果里面有 yield 第一次加括号调用, 他是一个生成器, 还没有真正执行,__next__()才会执行
- r = con.__next__()
- r = con2.__next__()
- n = 0
- while n<5:
- n += 1
- #send 有两个作用: 唤醒生成器的同时, 传送一个值, 传的这个值就是 yield 接收到的值 new_baozi
- con.send(n)
- con2.send(n)
- #time.sleep(1)
- print("\033[32;1m[producer]\033[0m is making baozi %s" %n)
- if __name__ == '__main__':
- con = consumer("c1")
- con2 = consumer("c2")
- p = producer()
整个就是通过 yield 实现的一个简单的协程, 在程序运行过程中, 直接完成运作, 感觉像多并发的效果
问题: 他们好像能够实现多并发的效果, 是因为每一个生产者没有任何的 sleep, 如果在 producer 中加一个 time.sleep(1), 那么运行速度就变慢了
遇到 io 操作就切换, io 操作很耗时, 协程之所以能够处理大并发, 就是把 io 操作去除, 之后就变成这个程序只有 cpu 在切换
如何实现程序自动检测 io 操作完成:
- greenlet
- from greenlet import greenlet
- def tesst1():
- print(12)
- gr2.switch()
- print(34)
- gr2.switch()
- def tesst2():
- print(56)
- gr1.switch()
- print(78)
- gr1 = greenlet(tesst1)# 启动一个协程
- gr2 = greenlet(tesst2)
- gr1.switch()# 手动切换
运行结果:
greenlet 现在还是手动切换
gevent
Gevent 是一个第三方库, 可以轻松通过 gevent 实现并发同步或异步编程, 在 gevent 中用到的主要模式是 Greenlet, 它是以 c 扩展模块形式接入 python 的轻量级协程 Greenlet 全部运行在主程序操作系统进程的内部, 但他们被协作式的调度
- import gevent
- def foo():
- print("Running in foo")
- gevent.sleep(2)
- print("Explicit context switch to foo again")
- def bar():
- print("Explicit context to bar")
- gevent.sleep(1)
- print("Implicit context to switch back to bar")
- gevent.joinall(
- [
- gevent.spawn(foo),
- gevent.spawn(bar),
- ]
- )
运行结果:
运行整个也就运行了 2 秒, 模拟 io 操作
下面我们做一个简单的小爬虫:
- from urllib.request import urlopen
- import gevent,time
- def f(url):
- print("GET: %s"%url)
- resp = urlopen(url)
- data = resp.read()
- print("%d bytes received from %s." %(len(data),url))
- urls = [
- 'https://www.python.org/',
- 'https://www.yahoo.com/',
- 'https://github.com/'
- ]
- time_start = time.time()
- for url in urls:
- f(url)
- print("同步 cost",time.time()-time_start)
- async_time_start = time.time()
- gevent.joinall([
- gevent.spawn(f,'https://www.python.org/'),
- gevent.spawn(f,'https://www.yahoo.com/'),
- gevent.spawn(f,'https://github.com/')
- ])
- print("异步 cost",time.time()-time_start)
用了同步和异步两种方式来进行网页的爬取, 但是结果却是同步的比异步的用的时间还要短, 我们之前用了 gevent.sleep(1)来模拟 io 操作, 顺便就完成了执行任务原因是: gevent 调用 urllib 默认是堵塞的, gevent 检测不到 urllib 的 io 操作, 所以不会进行切换, 所以还是串行运行
那怎么才能够让 gevent 知道 urllib 是 io 操作呢, 要用到一个模块 monkey
- from urllib.request import urlopen
- import gevent,time
- from gevent import monkey
- monkey.patch_all()# 把当前程序的所有 io 操作单独的坐上标记
- def f(url):
- print("GET: %s"%url)
- resp = urlopen(url)
- data = resp.read()
- print("%d bytes received from %s." %(len(data),url))
- urls = [
- 'https://www.python.org/',
- 'https://www.yahoo.com/',
- 'https://github.com/'
- ]
- time_start = time.time()
- for url in urls:
- f(url)
- print("同步 cost",time.time()-time_start)
- async_time_start = time.time()
- gevent.joinall([
- gevent.spawn(f,'https://www.python.org/'),
- gevent.spawn(f,'https://www.yahoo.com/'),
- gevent.spawn(f,'https://github.com/')
- ])
- print("异步 cost",time.time()-async_time_start)
这样就异步爬取的速度瞬间快了很多
下面我们在利用 gevent 写一个 socket 服务器端和客户端:
服务器端:
- import sys,socket,time,gevent
- from gevent import socket,monkey
- monkey.patch_all()
- def server(port):
- s = socket.socket()
- s.bind(('0.0.0.0',port))
- s.listen(500)
- while True:
- cli,addr = s.accept()
- gevent.spawn(handle_request,cli)
- def handle_request(conn):
- try:
- while True:
- data = conn.recv(1024)
- print("recv:",data)
- conn.send(data)
- if not data:
- conn.shutdown(socket.SHUT_WR)
- except Exception as e:
- print(e)
- finally:
- conn.close()
- if __name__ == '__main__':
- server(8001)
和 socketserver 差不多, 处理数据都是在 handle_request 函数中, 然后在 server 中建立一个 gevent
客户端:
- import socket
- HOST = 'localhost'
- PORT = 8001
- s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
- s.connect((HOST,PORT))
- while True:
- msg = bytes(input(">>>:"),encoding="utf-8")
- s.sendall(msg)
- data = s.recv(1024)
- #repr 格式化输出
- print("Recv:",repr(data))
- s.close()
运行结果:
由上面几个运行结果可以知道: 我们已经实行了并发运行, 不需要使用什么多线程我们用协程, 遇到 io 就阻塞
我们现在实现了切换, 但是我们是什么时候切换回来呢, 我们怎么知道什么时候这个函数的东西执行完, 切换到原函数呢
事件驱动与异步 IO
通常, 我们写服务器处理模型的程序时, 有一下几个模型:
1. 每收到一个请求, 创建一个新的进程, 来处理改请求: 如 socketserver
2. 每收到一个请求, 创建一个新的线程, 来处理该请求: 如 socketserver
3. 每收到一个请求, 放入一个事件列表, 让主进程通过非阻塞 I/O 方式来处理请求, 即事件驱动的模式, 该模式是大多数网络服务器采用的方式
在 ui 编程中, 我们常常要对鼠标点击进行相应的反应, 我们怎么获得鼠标点击呢?
目前大部分的 UI 编程都是事件驱动模型, 如很多的 ui 平台都会提供 onclick()事件, 这个事件就代表了鼠标按下的事件事件驱动模型大体思路:
1. 有一个事件队列
2. 鼠标按下时, 往这个队列中增加一个点击事件
3. 有个循环, 不断的从队列中取出事件, 根据不同的事件, 调用不同 的函数, 如: onclick(),onkeydown()等
4. 事件 (消息) 一般都各自保存各自的处理函数指针, 这样, 每个消息都有独立的处理函数
那我们在回到上面的问题, io 什么时候切换回来我们可以注册一个回调函数, 回调函数就是当你的程序一遇到 io 操作, 就切换, 然后等着 io 操作结束又切换回来 io 操作是操作系统完成的就是通过这个事件驱动
来源: https://www.cnblogs.com/charles8866/p/8445991.html