一 简介

1 同步和异步

函数或方法掉调用的时候,被调用者是否能得到最终结果来判断同步和异步
直接得到最终结果的,就是同步调用
不直接得到最终结果的,就是异步调用

同步就是我让你打饭,你不打好我就不走开,直到你打饭给了我
异步就是我让你打饭,你大着,我不等你,但是我会盯着你,你打完我会过来拿走,异步并不能保证多长时间将饭打完。

异步给的是临死结果,目前是拿不到的
同步只看结果是不是最终结果进行判断

2 阻塞,非阻塞

函数或方法调用的时候,是否立即返回
立即返回就是非阻塞调用
不立即返回就是阻塞调用

3 区别

同步,异步,阻塞,非阻塞 不相关
同步异步强调的是结果
阻塞,非阻塞强调的是时间,是否等待

同步和异步的区别在于:调用者是否得到可想要的结果

同步就是一直要执行到返回结果

异步就是直接返回了,但是不是最终结果,调用者不能通过这种调用方式得到结果,还是需要通过被调用者,使用其他方式通知调用者,来取回最终的结果

同步阻塞:我啥事也不干,就等你打饭给我,打饭是结果,而且我啥事也不敢就一直等,同步加阻塞。

同步非阻塞:我等着你打饭给我,但我可以完手机,看电视,打饭是结果,但我不一直等

异步阻塞: 我要打饭,你说等号,并没有给我返回饭,我啥事也不干,就等着饭好了叫我,叫号。

异步非阻塞:我要打饭,你说等号,并没有返回饭,我在旁边看电视,玩手机,反打好了叫我。

4 同步IO,异步IO,IO 多路复用

1 IO 两个阶段

1 数据准备阶段
2 内核空间复制会用户进程缓冲区阶段

2 发生IO的时候

1 内核从输入设备读写数据
2 进程从内核复制数据
系统调用read 函数
第一个IO阻塞的函数是input函数,是一个同步阻塞模型,网络也是一个IO,标准输入,标准输出等也IO

5 零拷贝

1 零拷贝概念

CPU 不执行拷贝数据从一个存储区域到另一个存储区域的任务,这通常用于通过网络传输一个文件时用于减少CPU周期和内存带宽。

操作系统某些组件(例如驱动程序、文件系统和网络协议栈)若采用零复制技术,则能极大地增强了特定应用程序的性能,并更有效地利用系统资源。通过使CPU得以完成其他而非将机器中的数据复制到另一处的任务,性能也得到了增强。另外,零复制操作减少了在用户空间与内核空间之间切换模式的次数。

零复制协议对于网络链路容量接近或超过CPU处理能力的高速网络尤为重要。在这种网络下,CPU几乎将所有时间都花在复制要传送的数据上,因此将成为使通信速率低于链路容量的瓶颈。

2 零拷贝带来的好处

1 减少甚至完全避免不必要的CPU拷贝,从而让CPU 解脱出来去执行其他任务
2 减少内存带宽占用
3 通常零拷贝技术还能减少用户空间和内核空间之间的上下文切换

3 Linux 系统的"用户空间"和"内核空间"

从Linux系统来看,除了引导系统的BIN区,整个内存空间主要被分成两部分:

1 内核空间(kernel space ) : 主要提供给程序调度,内存分配,连接硬件资源等程序逻辑空间

2 用户空间 (user space): 提供给各个进程的主要空间,用户空间不具备访问内核空间资源的权限,因此如果应用程序需要使用到内核空间的资源,则需要通过系统调度来完成,从用户空间切换到内核空间,然后在完成操作后再从内核空间切换到用户空间

4 Linux 中的零拷贝技术的实现方向

1 直接I/O: 对于这种传输方式来说,应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输,这种方式依旧存在用户空间和内核空间的上下文切换,但硬件上的数据不会拷贝到内核空间,而是直接拷贝到可用户空间,因此直接IO不存在内核空间缓冲区和用户空间缓冲区之间的数据拷贝


2 在数据传输过程中,避免数据在用户空间缓冲区和内核空间缓冲区之间的CPU拷贝,以及数据在系统内核空间的CPU拷贝,


3 copy-on-write(写时复制技术):在某些情况下,Linux操作系统的内核缓冲区可能被多个应用程序共享,操作系统有可能会将用户空间缓冲区地址映射考内核空间缓冲区,当应用程序需要对共享的数据进行修改时,才需要真正的拷贝数据到应用程序的用户空间缓冲区中,并且对自己的用户空间的缓冲区的数据进行修改不会影响到其他共享数据的应用程序,所以,如果应用程序不需要对数据进行任何修改,就不会存在数据从系统内核空间缓冲区拷贝到用户空间缓冲区的操作。

对于零拷贝技术是否实现主要依赖于操作系统底层是否提供相应的支持。

5 传统I/O 操作

1 发起read系统调用: 导致用户空间到内核空间的上下文切换(第一次上下文切换),通过DMA引擎将文件中的数据从磁盘上读取到内核空间缓冲区(第一次拷贝:hand drive ----> kernel buffer)


2 将内核空间缓冲区的数据拷贝到用户空间缓冲区中(第二次拷贝: kernel buffer ---> user buffer),然后read系统调用返回,而系统调用的返回又会导致一次内核空间到用户空间的上下文切换(第二次上下文切换)


3 发出write系统调用: 导致用户空间到内核空间的上下文切换(第三次上下文切换),将用户空间缓冲区的数据拷贝到内核空间中于socket相关的缓冲区中,(及第二步从内核空间缓冲区拷贝的数据原封不动的再次拷贝到内核空间的socket缓冲区中)( 第三次拷贝: user buffer--> socket buffer)


4 write 系统调用返回,导致内核空间到用户空间的再次上下文切换(第四次上下文切换),通过DMA引擎将内核缓冲区中的数据传递到协议引擎(第四次拷贝:socket buffer -> protocol engine ),这次拷贝时独立的异步的过程。


事实上调用的返回并不保证数据被传输,甚至不保证数据传输的开始,只是意味着将我么要发送的数据放入到了一个待发送的队列中,除非实现了优先环或者队列,否则会是先进先出的方式发送数据的。

总的来说,传统的I/O操作进行了4次用户空间与内核空间的上下文切换,以及4次数据拷贝。其中4次数据拷贝中包括了2次DMA拷贝和2次CPU拷贝。

传统模式为何将数据从磁盘读取到内核空间而不是直接读取到用户空间缓冲区,其原因是为了减少IO操作以提高性能,因为OS会根据局部性原理一次read() 系统调用的时候预读取更多的文件数据到内核空间缓冲区中,这样当下一次read()系统调用的时候发现要读取的数据已经存在于内核空间缓冲区的时候只需要直接拷贝数据到用户空间缓冲区即可,无需再进行一次低效的磁盘IO操作。

Bufferedinputstream 作用是会根据情况自动为我们预读取更多的数据到他自己维护的一个内部字节数据缓冲区,这样能减少系统调用次数来提高性能。

总的来说,内核缓冲区的一大作用是为了减少磁盘IO操做,Bufferedinputstream 则是减少"系统调用"

6 DMA

DMA(direct memory access) --- 直接内存访问,DMA 是允许外设组件将IO数据直接传送到主存储器并并且传输不需要CPU参与,以此解放CPU去做其他的事情。
而用户空间与内核空间之间的数据传输并没有类似DMA这种可以不需要CPU参与的传输工具,因此用户空间与内核空间之间的数据传输是需要CPU全程参与的。所有也就有了通过零拷贝技术来减少和避免不必要的CPU数据拷贝过程。

7 通过sendfile 实现零拷贝IO

1 发起sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换),通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive --> kernel buffer)然后再将数据从内核空间拷贝到socket相关的缓冲区中,(第二次拷贝,kernel ---buffer --> socket buffer)


2 sendfile 系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。通过DMA 引擎将内核空间的socket缓冲区的数据传递到协议引擎(第三次拷贝:socket buffer-> protocol engine )


总的来说,通过sendfile实现的零拷贝I/O只使用了2次用户空间与内核空间的上下文切换,以及3次数据的拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝。

Q:但通过是这里还是存在着一次CPU拷贝操作,即,kernel buffer ——> socket buffer。是否有办法将该拷贝操作也取消掉了?
A:有的。但这需要底层操作系统的支持。从Linux 2.4版本开始,操作系统底层提供了scatter/gather这种DMA的方式来从内核空间缓冲区中将数据直接读取到协议引擎中,而无需将内核空间缓冲区中的数据再拷贝一份到内核空间socket相关联的缓冲区中。

8 带有DMA 收集拷贝功能的sendfile 实现的IO

从Linux 2.4 开始,操做系统底层提供了带有scatter/gather 的DMA来从内核空间缓冲区中将数据读取到协议引擎中,这样以来待传输的数据可以分散再存储的不同位置,而不需要再连续存储中存放,那么从文件中读出的数据就根本不需要被拷贝到socket缓冲区中去,只是需要将缓冲区描述符添加到socket缓冲区中去,DMA收集操作会根据缓冲区描述符中的信息将内核空间中的数据直接拷贝到协议引擎中


1 发出sendfile 系统调用,导致用户空间到内核空间的上下文切换,通过DMA 引擎将磁盘文件内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive -> kernel buffer)


2 没有数据拷贝到socket缓冲区,取而代之的是只有向相应的描述信息被拷贝到相应的socket缓冲区中,该描述信息包含了两个方面: 1 kernel buffer 的内存地址 2 kernel buffer 的偏移量。


3 sendfile 系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换),DMA gather copy 根据 socket缓冲区中描述符提供的位置和偏移量信息直接将内核空间的数据拷贝到协议引擎上(kernel buffer --> protocol engine),这样就避免了最后依次CPU数据拷贝


总的来说,带有DMA收集拷贝功能的sendfile实现的I/O只使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,而且这2次的数据拷贝都是非CPU拷贝。这样一来我们就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换。

在Linux 2.6.33 版本之前sendfile支持文件到套接字之间的传输,及in_fd 相当于一个支持mmap的文件,out_fd 必须是一个socket,但从Linux 2.6.33版本开始,out_fd 可以是任意类型文件描述符,所以从Linux 2.6.33 版本开始sendfile 可以支持文件到文件,文件到套接字之间的数据传输。

9 传统I/O 和零拷贝及sendfile零拷贝I/O比较

传统I/O通过两条系统指令read、write来完成数据的读取和传输操作,以至于产生了4次用户空间与内核空间的上下文切换的开销;而sendfile只使用了一条指令就完成了数据的读写操作,所以只产生了2次用户空间与内核空间的上下文切换。
传统I/O产生了2次无用的CPU拷贝,即内核空间缓存中数据与用户空间缓冲区间数据的拷贝;而sendfile最多只产出了一次CPU拷贝,即内核空间内之间的数据拷贝,甚至在底层操作体系支持的情况下,sendfile可以实现零CPU拷贝的I/O。
因传统I/O用户空间缓冲区中存有数据,因此应用程序能够对此数据进行修改等操作;而sendfile零拷贝消除了所有内核空间缓冲区与用户空间缓冲区之间的数据拷贝过程,因此sendfile零拷贝I/O的实现是完成在内核空间中完成的,这对于应用程序来说就无法对数据进行操作了。
Q:对于上面的第三点,如果我们需要对数据进行操作该怎么办了?
A:Linux提供了mmap零拷贝来实现我们的需求

10 通过mmap 实现零拷贝I/O

Mmap(内存映射)是一个比sendfile昂贵但优于传统IO的方式

1 发出mmap系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。


2 mmap 系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换),接着用户空间和内核空间共享这个缓冲区,而不需要将数据从内核空间拷贝到用户空间,因此用户空间和内核空间共享的缓冲区


3 发出write 系统调用红,导致用户空间到内核空间第三次上下文切换,将数据从内核空间拷贝到内核空间的socket相关的缓冲区(第二次拷贝:kernel buffer ----> socket buffer )


4 write 系统调用返回,导致内核空间到用户空间的上下文切换(第四次上下文切换),通过DMA 引擎将内核空间socket缓冲区的数据传递到协议引擎(第三次拷贝: socket buffer---> protocol engine)

总的来说,通过mmap实现的零拷贝I/O进行了4次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝。

6 同步IO

1 同步阻塞IO

python同步编程和异步编程

在文件读取进入内核空间和从内核空间拷贝进入用户进程空间的过程中,没有任何的数据返回,客户端在一直等待状态。

2 同步非阻塞

python同步编程和异步编程

进程调用read操作,如果IO没有准备好,立即返回ERROR,进程不阻塞,用户可以再次发起系统调用,如果内核已经准备好,就阻塞,然后复制数据到用户空间

第一阶段数据没准备好,就先忙别的,等会再看看,检查数据是否准备好了的过程是非阻塞的

第二阶段是阻塞的,及内核空间和用户空间之间复制数据是阻塞的,但是要等待饭盛好才是完事,这是同步的。

3 IO 多路复用

python同步编程和异步编程

所谓的IO多路复用,就是同时监控多个IO,有一个准备好了,就不需要等待开始处理,提高了同时处理IO的能力

select是所有平台都支持,poll是对select的升级

epoll,Linux 系统内核2.5+ 开始支持,对select和epoll的增强,在监视的基础上,增加了回调机制,BSD,Mac的kqueue,还有windows的iocp

如果既想访问网络,又想访问文件,则先将准备好的数据先处理,那个准备好了就处理那个

能够提高同时处理IO的能力,谁先做玩我先处理谁

上面的两种方式,效率太差了,等完一个完成后再等一个,太慢了。

谁好了处理谁,不同的平台对IO多路复用的实现方式是不同的

Select 和 poll 在Linux,Windows,和MAC中都支持

一般来将select和poll 在同一个层次,epoll是Linux中存在的


select原理

1 将关注的IO操作告诉select函数并调用,进程阻塞,内核监视select关注的文件,描述符FD,被关注的任何一个FD对应的IO准备好了数据,select就返回,在使用read将数据复制到用用户进程。其select模式下的准备好的通知是没有针对性的,需要用户自己找到是否是自己的并进行处理。select做到的是时间重叠

epoll增加了回调机制,那一路准备好了,我会告诉你,有一种是你不用管了,好了我直接替你调用。

7 异步调用

python同步编程和异步编程

两个阶段
等待数据准备和拷贝阶段
立即返回数据,给一个号。到时候叫号,直接返回
信号句柄,告诉你几号好了,(signal handler process datagram)
有些时候是需要争抢的
我可以不通知你,我也可以通知你后你再来
理解数据层面的东西,就不要理解其他的socket层面的东西
文件中实际就是两个缓冲队列,每个队列是一个。
在异步模型中,操作系统通你的,你是在用户空间的,操作系统可以是在内核空间的,进程和线程等等的都是操作系统层面的东西。
整个过程中进程都可以做其他的事,就算是通知了,也不一定要立即反应,这和你的设置有关
Linux中的AIO 的系统调用,内核版本从2.6开始支持
一般的IO是IO多路复用和异步复用

二 python中的IO 多路复用

1 简介

IO 多路复用
大多数操作系统都支持select和poll
Linux 2.5+ 支持epoll
BSD,Mac支持kqueue
Windows 的 iocp
python的select库

实现了select,poll系统调用,这个基本上操作系统都支持,部分实现了epoll,底层的IO多路复用模块

开发中的选择

1 完全跨平台,select 和poll ,但其性能较差
2 针对不同的操作系统自行选择支持技术,这样会提高IO处理能力

selectors库
3.4 版本后提供这个库,高级的IO复用库
类层次结构

BaseSelector
+-- SelectSelector 实现select
+-- PollSelector 实现poll
+-- EpollSelector 实现epoll
+-- DevpollSelector 实现devpoll
+-- KqueueSelector 实现kqueue

selectors.DefaultSelector返回当前平台最有效,性能最最高的实现
但是由于没有实现windows的IOCP,所以只能退化为select。
默认会自适应,其会选择最佳的方式,Linux 会直接选择 epoll ,通过此处,能拿到平台的最优方案。

DefaultSelector 源码

if 'KqueueSelector' in globals():
    DefaultSelector = KqueueSelector
elif 'EpollSelector' in globals():
    DefaultSelector = EpollSelector
elif 'DevpollSelector' in globals():
    DefaultSelector = DevpollSelector
elif 'PollSelector' in globals():
    DefaultSelector = PollSelector
else:
    DefaultSelector = SelectSelector

2 基本方法

abstractmethod register(fileobj,events,data=None)

为selection注册一个文件独享,监视它的IO事件
fileobj 被监视的文件对象,如socket对象
events 事件,该文件对象必须等待的事件,read或write

python同步编程和异步编程

data 可选的与此文件对象相关的不透明数据,如可用来存储每个客户端的会话ID,可以是函数,类,实例,如果是函数,有点回调的意思,通知某个函数,某个实例,某个类,可以是类属性,等,都可以,None表示消息发生了,没人认领。

3 基本实现socket 操作监控

1 思路

第一步 :需要实例化 ,选择一个最优的实现,将其实例化(选择不同平台实现的IO复用的最佳框架),python内部处理


第二步:注册函数,将要监控对象,要监控事件和监控触发后对象写入register注册中

1 注册: 对象,啥事件,调用的函数
2 进行循环和监控select函数的返回,当监控的对象的事件满足时会立即返回,在events中可以拿到这些数据events中有我是谁,我是什么事件触发的(读和写),读的满足可以recv,key 是让我监控的东西,event是其什么事件触发的。将对象和事件拿到后做相应的处理。


第三步:实时关注socket有读写操作,从而影响events的变化

对socket来判断有没有读,若读了,则直接触发对应的机制进行处理。一旦有新的连接准备,则会将其消息发送给对应的函数进行处理相关的操作。被调用的函数是有要求的,其需要传送mask的,data 就是未来要调用的函数,建立了事件和未来参数之间建立的关系。

Accept 本身就是一个read事件
Selector 会调用自己的select函数进行监视,这个函数是阻塞的,当数据已经在内核缓冲区准备好了,你就可以读取了,这些事给select进行处理

在注册的时候,后面加了data,后面直接使用,直接调用,不用管其他,data和每一个观察者直接对应起来的。

只要有一个满足要求,直接返回

读事件指的是in操作,及就是当有连接的时候

当通知成功后,其函数内部是不会阻塞了,等待通知,通知成功后就不会阻塞了。此处的data相当于直接带着窗口号,直接进行处理,而不需要一个一个的遍历

当一个满足了,就不会阻塞了。events: 两个IO都满足,等待几路,几路的IO都在此处,如果满足,则直接向下打印events,其中key是注册的唯一的东西,socket 也可以,但是可以定义socket的读和写,一般都是合着的


第四步:调用对应事件的对象,并执行相关操作

然后将events拿出来解构,key本身是一个多元祖,key上保存着注册塞进去的data,key是存储了4个信息的元祖,此处的data称为回调函数,加上() 称为调用

2 代码实现

python同步编程和异步编程
python同步编程和异步编程

代码下载目录
IO 多路复用初始代码

https://pan.baidu.com/s/18B5OL89Z4YSxEmX4gNkgDA

3 基本参数讲解

1 events参数:

2019-09-01 09:37:46 Thread-1 events: [(SelectorKey(fileobj=<socket.socket fd=4, family=AddressFamily.AF_INET, type=2049, proto=0, laddr=('192.168.1.200', 9999)>, fd=4, events=1, data=<function accept at 0x7f50feb61d90>), 1)]

events中包含了两组
第一组 :
fileobj 及套接字返回的相关参数,和之前的socket中的accpet中的conn 相似,

fd 及文件描述符

events 及事件类型,python同步编程和异步编程两种

data 及注册调用的函数,上述的有accept 和recv 函数


第二组:
1 events 的状态,及mask

2 select.get_map() 参数:

1 select.get_map().items() 中的key
2019-09-01 09:43:52 MainThread key:SelectorKey(fileobj=<socket.socket fd=4, family=AddressFamily.AF_INET, type=2049, proto=0, laddr=('192.168.1.200', 9999)>, fd=4, events=1, data=<function accept at 0x7fcf5a50ad90>)

此处的key和上面的列表中的二元祖中的前一个完全相同


2 select.get_map().items() 中的fobj
2019-09-01 09:43:52 MainThread fobj: 4
其是其中的文件描述符

4 总结:

IO 多路复用就是一个线程来处理所有的IO
在单线程中进行处理IO多路复用
多线程中的IO阻塞时浪费CPU资源,其是等待状态,等待状态虽然不占用CPU资源,但线程本身的状态需要维持,还是会占用一定的资源

4 改进版本的socket 监控

1 描述

send 是写操作,有可能阻塞,也可以监听
recv所在的注册函数,要监听python同步编程和异步编程读与写事件,回调的时候,需要mask 来判断究竟是读触发了还是写触发了,所以,需要修改方法声明,增加mask
写操作当发送群聊时,其每个链接是独立的,需要queue队列保存相关的数据,并进行接受和发送操作

2 代码实现

python同步编程和异步编程
python同步编程和异步编程

python同步编程和异步编程

IO 多路复用最终代码

https://pan.baidu.com/s/1y-3j607_5DxBpa4wZNxCEQ

三 异步编程

1 asyncio 简介

3.4 版本加入标准库
asyncio 底层是基于selectors实现的,看似库,其实就是一个框架,包括异步IO,事件循环,协程,任务等


并行和串行的区分:
两个事件的因果关系:
若有因果关系,则可以使用串行
若无因果关系,则可以使用并行,及多线程来处理

2 相关参数及详解

参数 含义
asyncio.get_event_loop() 返回一个事件循环对象,是asyncio.BaseEventLoop的实例
AbstractEventLoop.stop() 停止运行事件循环
AbstractEventLoop.run_forever() 一直运行,直到stop()
AbstractEventLoop.run_until_complete(future) 运行直到Future对象运行完成
AbstractEventLoop.close() 关闭事件循环
AbstractEventLoop.is_running() 返回事件循环是否运行
AbstractEventLoop.close() 关闭事件

3 协程

1 基本实例

#!/usr/bin/poython3.6
#conding:utf-8
import   threading
def  a():
    for i in range(3):
        print (i)

def  b():
    for  i  in "abc":
        print (i)

a()
b()

python同步编程和异步编程

此处的默认执行顺序是a()到b()的顺序执行,若要使其交叉执行,则需要使用yield 来实现

实现方式如下

#!/usr/bin/poython3.6
#conding:utf-8
import   threading
import multiprocessing
def  a():
    for i in range(3):
        print (i)
        yield

def  b():
    for  i  in "abc":
        print (i)
        yield

a=a()
b=b()
for  i in range(3):
    next(a)
    next(b)

python同步编程和异步编程

上述实例中通过生成器完成了调度,让两个函数都几乎同时执行,这样的调度不是操作系统进行的。而是用户自己设计完成的


这个程序编写要素:
1 需要使用yield来让出控制权
2 需要循环帮助执行

2 协程简介

协程不是进程,也不是线程,它是用户空间调度的完成并发处理的方式。
进程,线程由操作系统完成调度,而协程是线程内完成调度的,不需要更多的线程,自然也没有多线程切换的开销
协程是非抢占式调度,只有一个协程主动让出控制权,另一个协程才会被调度。
协程也不需要使用锁机制,因为其是在同一个线程中执行的
多CPU下,可以使用多进程和协程配合,既能进程并发,也能发挥出协程在单线程中的优势。
python中的协程是基于生成器的。

3 协程基本书写

3.4 引入的asyncio,使用装饰器

#!/usr/bin/poython3.6
#conding:utf-8
import   threading
import multiprocessing
import asyncio
@asyncio.coroutine
def  a():
    for i in range(3):
        print (i)
        yield
loop=asyncio.get_event_loop()
loop.run_until_complete(a())
loop.close()

结果如下

python同步编程和异步编程

#!/usr/bin/poython3.6
#conding:utf-8
import   threading
import multiprocessing
import asyncio
@asyncio.coroutine
def  a():
    for i in range(3):
        print (i)
        yield
@asyncio.coroutine
def b():
    for  i in "abc":
        print(i)
        yield

loop=asyncio.get_event_loop()
task=[a(),b()]
loop.run_until_complete(asyncio.wait(task))
loop.close()

结果如下

python同步编程和异步编程

3.5 及其以后版本的书写方式:

#!/usr/bin/poython3.6
#conding:utf-8
import   threading
import multiprocessing
import asyncio
async  def   a():
    for i in range(3):
        print (i)
        # await  asyncio.sleep(0.0001)
async  def   b(): #使用此方式后,不能再次使用wait了
    for  i in "abc":
        print(i)
        # await  asyncio.sleep(0.0001)

print (asyncio.iscoroutinefunction(a)) # 此处判断是否是函数,和调用无关

a=a()
print (asyncio.iscoroutine(a))  # 此处是判断对象,是调用后的结果
loop=asyncio.get_event_loop()
task=[a,b()]
loop.run_until_complete(asyncio.wait(task))
loop.close()

结果如下

python同步编程和异步编程

async def 用来定义协程函数,iscoroutinefunction()返回True,协程函数中可以不包含await,async关键字,但是不能使用yield关键字
如果生成器函数调用返回生成器对象一样,协程函数调用也会返回一个对象成为协程对象,iscoroutine()返回为True

4 TCP echo server 实现

#!/usr/bin/poython3.6
#conding:utf-8
import   threading
import multiprocessing
import asyncio
import  socket
ip='192.168.1.200'
port=9999
async def  handler(conn,send):
    while True:
        data=await   conn.read(1024) # 接受客户端的数据,相当于recv,wait 就是IO等待,此处会等待
        print (conn,send)
        client_addr=send.get_extra_info('peername')  # 获取客户端信息
        msg="{} {}".format(data.decode(),client_addr).encode()  #封装消息
        send.write(msg)  # 传输到客户端
        await    send.drain()  # 此处相当于makefile中的flush ,此处也会IO等待

loop=asyncio.get_event_loop() #实例化一个循环事件
crt=asyncio.start_server(handler,ip,port,loop=loop) #使用异步方式启动函数,最后一个参数是应该用谁来循环处理
server=loop.run_until_complete(crt) # 此处是直到此方法完成后终止

print (server)
try:
    loop.run_forever()
except KeyboardInterrupt:
    pass
finally:
    server.close()
    loop.close()

5 扩展aiohttp 库

异步的http 库,使用协程实现的
需要安装第三方模块 aiohttp

pip  install  aiohttp 

http server 基础实现

#!/usr/bin/poython3.6
#conding:utf-8

from  aiohttp  import web
async def  indexhandle(request:web.Request):  # 处理客户端请求函数
    print("web",web.Request)
    return web.Request(text=request.path,status=201) #返回文本和状态码

async def  handle(request:web.Request):
    print (request.match_info)
    print (request.query_string)
    return web.Response(text=request.match_info.get('id','0000'),status=200)  # 此处是返回给客户端的数据,后面的0000是默认

app=web.Application()

#路由选路,
app.router.add_get('/',indexhandle) # http://192.168.1.200:80/
app.router.add_get('/{id}',handle)  # http://192.168.1.200:80/12345

web.run_app(app,host='0.0.0.0',port=80)  #监听IP和端口并运行

客户端实现

#!/usr/bin/poython3.6
#conding:utf-8
import asyncio
from aiohttp import ClientSession

async  def  get_html(url:str):
    async with   ClientSession()  as  session:  # 获取session,要和服务端通信,必须先获取session,之后才能进行相关的操作 ,此处使用with是打开关闭会话,保证会话能够被关闭。
        async with  session.get(url)  as res:  # 需要这个URL资源,获取,
            print (res.status)  # 此处返回为状态码
            print (await  res.text())  # 此处返回为文本信息

url='http://www.baidu.com'

loop=asyncio.get_event_loop()
loop.run_until_complete(get_html(url))
loop.close()