(见右侧目录栏导航)
python
- 1. 前言
- 2. IO的五种模型
- 3. 协程
- 3.1 协程的概念
- 4. Gevent 模块
- 4.1 gevent 基本使用
- 4.2 gevent应用一:爬虫
- 4.3 gevent应用二:网络编程
编程
CPU的速度远远快于磁盘、网络等IO。在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操做,如读写文件、发送网络数据时,就须要等待IO操做完成,才能继续进行下一步操做。这种状况称为同步IO。在IO操做的过程当中,当前线程被挂起,而其余须要CPU执行的代码就没法被当前线程执行了。由于一个IO操做就阻塞了当前线程,致使其余代码没法执行,因此咱们必须使用多线程或者多进程来并发执行代码,为多个用户服务。每一个用户都会分配一个线程,若是遇到IO致使线程被挂起,其余用户的线程不受影响。多线程和多进程的模型虽然解决了并发问题,可是系统不能无上限地增长线程。因为系统切换线程的开销也很大,因此,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果致使性能严重降低。因为咱们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法,另外一种解决IO问题的方法是异步IO。当代码须要执行一个耗时的IO操做时,它只发出IO指令,并不等待IO结果,而后就去执行其余代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。网络
(1)blocking IO (阻塞IO)多线程
(2)noblocking IO (非阻塞IO)并发
(3)IO multiplexing (IO多路复用)异步
(4)signal driven IO(信号驱动IO) -- 不经常使用socket
(5)asynchronous IO (异步IO)async
在理解上面五种IO模式以前须要理解如下4个概念:ide
同步、异步、阻塞、非阻塞异步编程
2.1 同步和异步
同步和异步关注的是消息通讯机制
同步:在发出一个调用时,没获得结果以前,该调用就不返回。可是一旦调用返回就获得返回值(结果)了,调用者须要主动等待这个调用的结果。
异步:在发送一个调用时,这个调用就直接返回了,无论返回有没有结果。当一个异步过程调用发出后,被调用者经过状态,通知调用者,或者经过回调函数处理这个调用
2.2 阻塞和非阻塞
阻塞和非阻塞关注的是程序在等待调用结果时的状态
阻塞:调用结果返回以前,当前线程会被挂起。调用线程只有在获得结果以后才返回;
非阻塞:在不能当即获得结果以前,该调用不会挂起当前线程
有一个很好的例子说明这4者之间的关系:
老张爱喝茶,废话不说,煮开水。 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1 老张把水壶放到火上,立等水开。(同步阻塞) 老张以为本身有点傻
2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞) 老张仍是以为本身有点傻,因而变高端了,买了把会响笛的那种水壶。水开以后,能大声发出嘀~~~~的噪音。
3 老张把响水壶放到火上,立等水开。(异步阻塞) 老张以为这样傻等意义不大
4 老张把响水壶放到火上,去客厅看电视,水壶响以前再也不去看它了,响了再去拿壶。(异步非阻塞) 老张以为本身聪明了。
所谓同步异步,只是对于水壶而言。 普通水壶,同步;响水壶,异步。 虽然都能干活,但响水壶能够在本身完工以后,提示老张水开了。这是普通水壶所不能及的。 同步只能让调用者去轮询本身(状况2中),形成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言。 立等的老张,阻塞;看电视的老张,非阻塞。 状况1和状况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。因此通常异步是配合非阻塞使用的,这样才能发挥异步的效用。
3.1 协程的概念
进程是资源分配的最小单位,线程是CPU调度的基本单位, 在Cpython中,因为GIL锁的存在,通常来讲,同一时间片只有一个线程在cpu中运行,为了提升单线程的效率,这里提出了协程的概念。
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序本身控制调度的。
须要强调:
1. python的线程属于内核级别的,即由操做系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其余线程运行)
2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操做系统)控制切换,以此来提高效率(!!!非io操做的切换与效率无关)
对比操做系统控制线程的切换,用户在单线程内控制协程的切换
优势以下:
1. 协程的切换开销更小,属于程序级别的切换,操做系统彻底感知不到,于是更加轻量级
2. 单线程内就能够实现并发的效果,最大限度地利用cpu
缺点以下:
1. 协程的本质是单线程下,没法利用多核,能够是一个程序开启多个进程,每一个进程内开启多个线程,每一个线程内开启协程
2. 协程指的是单个线程,于是一旦协程出现阻塞,将会阻塞整个线程
总结协程的特色:
1. 必须在只有一个单线程里实现并发
2. 修改共享数据不需加锁
3. 用户程序里本身保存多个控制流的上下文栈
4. 一个协程遇到IO操做自动切换到其余协程
4.1 gevent 基本使用
Gevent 是一个第三方库,能够轻松经过gevent实现并发同步或异步编程。
g1=gevent.spawn(func,1,,2,3,x=4,y=5) 建立一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面能够有多个参数,能够是位置实参或关键字实参,都是传给函数eat的 g2=gevent.spawn(func2) g1.join() 等待g1结束 g2.join() 等待g2结束 或者上述两步合做一步: gevent.joinall([g1,g2]) g1.value 拿到func1的返回值
使用gevent 遇到IO就切换实例:
import gevent def eat(): print('eat start...') gevent.sleep(2) print('eat end.') def play(): print('play start...') gevent.sleep(2) print('play end.') if __name__ == '__main__': g1 = gevent.spawn(eat) g2 = gevent.spawn(play) g1.join() g2.join() print('----主-----')
上例gevent.sleep(2)模拟的是gevent能够识别的io阻塞,而time.sleep(1)或其余的阻塞,gevent是不能直接识别的须要用下面一行代码,打补丁,就能够识别了
from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块以前或者咱们干脆记忆成:要用gevent,须要将from gevent import monkey;monkey.patch_all()放到文件的开头
from gevent import monkey; monkey.patch_all() import gevent import time def eat(): print('eat start...') time.sleep(2) print('eat end.') def play(): print('play start...') time.sleep(2) print('play end.') if __name__ == '__main__': g1 = gevent.spawn(eat) g2 = gevent.spawn(play) g1.join() g2.join() print('----主-----')
咱们能够用threading.current_thread().getName()来查看每一个g1和g2,查看的结果为DummyThread-n,即假线程
from gevent import monkey; monkey.patch_all() import threading import gevent import time def eat(): print(threading.current_thread().name) print('eat start...') time.sleep(2) print('eat end.') def play(): print(threading.current_thread().name) print('play start...') time.sleep(2) print('play end.') if __name__ == '__main__': g1 = gevent.spawn(eat) g2 = gevent.spawn(play) g1.join() g2.join() print('----主-----') 执行结果: DummyThread-1 eat start... DummyThread-2 play start... (阻塞2秒) eat end. play end. ----主-----
4.2 gevent 应用一:爬虫
from gevent import monkey; monkey.patch_all() import gevent import requests def get(url): print('GET:', url) response = requests.get(url) if response.status_code == 200: print('%d bytes recevied from %s' % (len(response.text), url)) if __name__ == '__main__': gevent.joinall([ gevent.spawn(get, 'https://www.baidu.com'), gevent.spawn(get, 'https://www.taobao.com'), gevent.spawn(get, 'https://www.jd.com')])
4.3 gevent 应用二:网络编程
经过gevent实现单线程下的socket并发
注意:from gevent import monkey;monkey.patch_all()必定要放到导入socket模块以前,不然gevent没法识别socket的阻塞
from gevent import spawn, monkey;monkey.patch_all() import socket def server(ip_port): sk_server = socket.socket() sk_server.bind(ip_port) sk_server.listen(5) while True: conn, addr = sk_server.accept() spawn(walk, conn) def walk(conn): conn.send(b'welcome!') try: while True: res = conn.recv(1024) print(res) conn.send(res.upper()) except Exception as e: print(e) finally: conn.close() if __name__ == '__main__': server(('localhost', 8080))
import socket sk_client = socket.socket() sk_client.connect(('localhost', 8080)) res = sk_client.recv(1024) print(res) while True: inp = input('>>>').strip() if not inp: continue sk_client.send(inp.encode()) print(sk_client.recv(1024))