异步IO\数据库\队列\缓存
1 # 异步IO\数据库\队列\缓存 2 # 1.协程:线程上下文切换会保存到cpu的寄存器里 协程是由用户自己实现的 3 # 2.函数中有yield时候会变成生成器 最简单的协程 4 # 3.send有两个作用: 唤醒生成器并传递数据 (交互) 5 # 4.遇到IO操作就切换 协程处理并发的原理就是避开IO操作 6 # IO操作完成切回去 IO操作系统负责操作系统的控制 7 # 5.greenlet 手动切换 8 # 6.gevent 自动切换 9 # 7.RabbitMQ 消息队列 10 # 8.redis 11 # 9.Mysql 12 13 ########################################################################## 14 # 1.greenlet手动切换 15 # from greenlet import greenlet 16 # 17 # def test1(): 18 # print(1) 19 # gr2.switch() 20 # print(2) 21 # gr2.switch() 22 # 23 # def test2(): 24 # print(3) 25 # gr1.switch() 26 # print(4) 27 # 28 # gr1 = greenlet(test1) 29 # gr2 = greenlet(test2) 30 # gr1.switch() 31 ########################################################################## 32 # 2.gevent自动切换 33 # import gevent 34 # def foo(): 35 # print("foo1") 36 # gevent.sleep(5) 37 # print("foo2") 38 # 39 # def bar(): 40 # print("bar1") 41 # gevent.sleep(2) 42 # print("bar2") 43 # 44 # def func(): 45 # print("func1") 46 # gevent.sleep(0.5) 47 # print("func2") 48 # 49 # gevent.joinall([ 50 # gevent.spawn(foo), 51 # gevent.spawn(bar), 52 # gevent.spawn(func) 53 # ]) 54 ########################################################################## 55 # 3.并发下的网页爬取 56 # from urllib import request 57 # import gevent, time 58 # from gevent import monkey 59 # monkey.patch_all() 60 # 61 # def f(url): 62 # print("GET: %s" % url) 63 # resp = request.urlopen(url) 64 # print(resp) 65 # data = resp.read() 66 # print("%s bytes received from %s" % (len(data), url)) 67 # 68 # urls = [ 69 # 'https://www.python.org/', 70 # 'https://www.baidu.com/', 71 # 'https://www.hao123.com/?tn=93730506_hao_pg', 72 # ] 73 # time_start = time.time() 74 # for url in urls: 75 # f(url) 76 # print("串行时间: ", time.time()-time_start) 77 # async_time = time.time() 78 # gevent.joinall([ 79 # gevent.spawn(f, 'https://www.python.org/'), 80 # gevent.spawn(f, 'https://www.baidu.com/'), 81 # gevent.spawn(f, 'https://www.hao123.com/?tn=93730506_hao_pg'), 82 # ]) 83 # print("并行时间: ", time.time()-async_time) 84 ########################################################################## 85 # 4.异步IO and 事件驱动 86 # 在进行解释之前,首先要说明几个概念: 87 # - 用户空间和内核空间: 88 # 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。 89 # - 进程切换: 90 # 为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。 91 # 从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化: 92 # 1. 保存处理机上下文,包括程序计数器和其他寄存器。 93 # 2. 更新PCB信息。 94 # 3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。 95 # 4. 选择另一个进程执行,并更新其PCB。 96 # 5. 更新内存管理的数据结构。 97 # 6. 恢复处理机上下文。 98 # - 进程的阻塞: 99 # 正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。 100 # - 文件描述符: 101 # 文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。 102 # 文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。 103 # - 缓存I/O: 104 # 缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。 105 # 缓存 I/O 的缺点: 106 # 数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。 107 # 刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段: 108 # 1. 等待数据准备 (Waiting for the data to be ready) 109 # 2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 110 # 111 # 正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。 112 # - 阻塞 I/O(blocking IO) 113 # - 非阻塞 I/O(nonblocking IO) 114 # - I/O 多路复用( IO multiplexing) 115 # - 信号驱动 I/O( signal driven IO) 116 # - 异步 I/O(asynchronous IO) 117 118 ########################################################################## 119 # 5.同步IO和异步IO,阻塞IO和非阻塞IO 120 # select poll epoll 121 # IO多路复用 122 # 理论上 1个链接4k 1G内存10w并发``` 123 # IO多路复用 如何使用? 124 125 # blocking和non-blocking的区别 126 # 调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回 127 # synchronous IO和asynchronous IO的区别 128 # 在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的: 129 # - A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes; 130 # - An asynchronous I/O operation does not cause the requesting process to be blocked; 131 # 两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。 132 #non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。 133 # 而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。 134 135 # IO多路复用之select、poll、epoll详解 136 # select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间 137 # select(rlist, wlist, xlist, timeout=None) 138 # 139 # int poll (struct pollfd *fds, unsigned int nfds, int timeout); 140 # 141 # int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大 142 # int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 143 # int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 144 145 #_*_coding:utf-8_*_ 146 147 # import socket, logging 148 # import select, errno 149 # 150 # logger = logging.getLogger("network-server") 151 # 152 # def InitLog(): 153 # logger.setLevel(logging.DEBUG) 154 # 155 # fh = logging.FileHandler("network-server.log") 156 # fh.setLevel(logging.DEBUG) 157 # ch = logging.StreamHandler() 158 # ch.setLevel(logging.ERROR) 159 # 160 # formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 161 # ch.setFormatter(formatter) 162 # fh.setFormatter(formatter) 163 # 164 # logger.addHandler(fh) 165 # logger.addHandler(ch) 166 # 167 # 168 # if __name__ == "__main__": 169 # InitLog() 170 # 171 # try: 172 # # 创建 TCP socket 作为监听 socket 173 # listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) 174 # except socket.error as msg: 175 # logger.error("create socket failed") 176 # 177 # try: 178 # # 设置 SO_REUSEADDR 选项 179 # listen_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 180 # except socket.error as msg: 181 # logger.error("setsocketopt SO_REUSEADDR failed") 182 # 183 # try: 184 # # 进行 bind -- 此处未指定 ip 地址,即 bind 了全部网卡 ip 上 185 # listen_fd.bind(('', 2003)) 186 # except socket.error as msg: 187 # logger.error("bind failed") 188 # 189 # try: 190 # # 设置 listen 的 backlog 数 191 # listen_fd.listen(10) 192 # except socket.error as msg: 193 # logger.error(msg) 194 # 195 # try: 196 # # 创建 epoll 句柄 197 # epoll_fd = select.epoll() 198 # # 向 epoll 句柄中注册 监听 socket 的 可读 事件 199 # epoll_fd.register(listen_fd.fileno(), select.EPOLLIN) 200 # except select.error as msg: 201 # logger.error(msg) 202 # 203 # connections = {} 204 # addresses = {} 205 # datalist = {} 206 # while True: 207 # # epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待 208 # epoll_list = epoll_fd.poll() 209 # 210 # for fd, events in epoll_list: 211 # # 若为监听 fd 被激活 212 # if fd == listen_fd.fileno(): 213 # # 进行 accept -- 获得连接上来 client 的 ip 和 port,以及 socket 句柄 214 # conn, addr = listen_fd.accept() 215 # logger.debug("accept connection from %s, %d, fd = %d" % (addr[0], addr[1], conn.fileno())) 216 # # 将连接 socket 设置为 非阻塞 217 # conn.setblocking(0) 218 # # 向 epoll 句柄中注册 连接 socket 的 可读 事件 219 # epoll_fd.register(conn.fileno(), select.EPOLLIN | select.EPOLLET) 220 # # 将 conn 和 addr 信息分别保存起来 221 # connections[conn.fileno()] = conn 222 # addresses[conn.fileno()] = addr 223 # elif select.EPOLLIN & events: 224 # # 有 可读 事件激活 225 # datas = '' 226 # while True: 227 # try: 228 # # 从激活 fd 上 recv 10 字节数据 229 # data = connections[fd].recv(10) 230 # # 若当前没有接收到数据,并且之前的累计数据也没有 231 # if not data and not datas: 232 # # 从 epoll 句柄中移除该 连接 fd 233 # epoll_fd.unregister(fd) 234 # # server 侧主动关闭该 连接 fd 235 # connections[fd].close() 236 # logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1])) 237 # break 238 # else: 239 # # 将接收到的数据拼接保存在 datas 中 240 # datas += data 241 # except socket.error as msg: 242 # # 在 非阻塞 socket 上进行 recv 需要处理 读穿 的情况 243 # # 这里实际上是利用 读穿 出 异常 的方式跳到这里进行后续处理 244 # if msg.errno == errno.EAGAIN: 245 # logger.debug("%s receive %s" % (fd, datas)) 246 # # 将已接收数据保存起来 247 # datalist[fd] = datas 248 # # 更新 epoll 句柄中连接d 注册事件为 可写 249 # epoll_fd.modify(fd, select.EPOLLET | select.EPOLLOUT) 250 # break 251 # else: 252 # # 出错处理 253 # epoll_fd.unregister(fd) 254 # connections[fd].close() 255 # logger.error(msg) 256 # break 257 # elif select.EPOLLHUP & events: 258 # # 有 HUP 事件激活 259 # epoll_fd.unregister(fd) 260 # connections[fd].close() 261 # logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1])) 262 # elif select.EPOLLOUT & events: 263 # # 有 可写 事件激活 264 # sendLen = 0 265 # # 通过 while 循环确保将 buf 中的数据全部发送出去 266 # while True: 267 # # 将之前收到的数据发回 client -- 通过 sendLen 来控制发送位置 268 # sendLen += connections[fd].send(datalist[fd][sendLen:]) 269 # # 在全部发送完毕后退出 while 循环 270 # if sendLen == len(datalist[fd]): 271 # break 272 # # 更新 epoll 句柄中连接 fd 注册事件为 可读 273 # epoll_fd.modify(fd, select.EPOLLIN | select.EPOLLET) 274 # else: 275 # # 其他 epoll 事件不进行处理 276 # continue 277 # 278 # epoll socket echo server 279 ########################################################################## 280 # 6.非阻塞模式下的多路复用 281 # select 282 # selector 283 ########################################################################## 284 # 7.RabbitMQ 285 286 ########################################################################## 287 # 8.Radis 288 289 ########################################################################## 290 # 9.Mysql