协程 和 io模型

# ==========       协程
# 协程介绍
# 协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。
# 一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。、

#强调的
#1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,
#切换其他线程运行)
#2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率
# (!!!非io操作的切换与效率无关)


#对比操作系统控制线程的切换,用户在单线程内控制协程的切换
# 优点
#1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
#2. 单线程内就可以实现并发的效果,最大限度地利用cpu

#缺点
#1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
#2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程


#总结协程特点:

# 1 必须在只有一个单线程里实现并发
# 2 修改共享数据不需加锁
# 3 用户程序里自己保存多个控制流的上下文栈
# 4 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))

## 进程 启动多个进程 进程之间是由操作系统负责调用
# 线程 启动多个线程 真正被CPU执行的最小单位实际是线程
# 开启一个线程 创建一个线程 寄存器 堆栈
# 关闭一个线程
# 协程
# 本质上是一个线程
# 能够在多个任务之间切换来节省一些IO时间
# 协程中任务之间的切换也消耗时间,但是开销要远远小于进程线程之间的切换
# 实现并发的手段
# import time
# def consumer():
# while True:
# x = yield
# time.sleep(1)
# print('处理了数据 :',x)
#
# def producer():
# c = consumer()
# next(c)
# for i in range(10):
# time.sleep(1)
# print('生产了数据 :',i)
# c.send(i)
#
# producer()

# greenlet 模块
# greenlet只是提供了一种比generator更加便捷的切换方式,
# 当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。

# 真正的协程模块就是使用greenlet完成的切换
#g1.switch('egon')#可以在第一次switch时传入参数,以后都不需要


# from greenlet import greenlet
# def eat():
# print('eating start')
# g2.switch()
# print('eating end')
# g2.switch()
#
# def play():
# print('playing start')
# g1.switch()
# print('playing end')
# g1 = greenlet(eat)
# g2 = greenlet(play)
# g1.switch()
# from gevent import monkey;monkey.patch_all()
# import time
# import gevent
# import threading
# def eat():
# print(threading.current_thread().getName())
# print(threading.current_thread())
# print('eating start')
# time.sleep(1)
# print('eating end')
#
# def play():
# print(threading.current_thread().getName())
# print(threading.current_thread())
# print('playing start')
# time.sleep(1)
# print('playing end')
#
# g1 = gevent.spawn(eat)
# g2 = gevent.spawn(play)
# g1.join()
# g2.join()


# gevent 模块
# 进程和线程的任务切换右操作系统完成

# 协程任务之间的切换由程序(代码)完成,只有遇到协程模块能识别的IO操作的时候,程序才会进行任务切换,实现并发的效果
# 上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,而time.sleep(2)或其他的阻塞,\
# gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了
# from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前
# 或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头

# gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet,
# 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。


# 方法
# 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的返回值

# 同步 和 异步
# from gevent import monkey;monkey.patch_all()
# import time
# import gevent
#
# def task(n):
# time.sleep(1)
# print(n)
#
# def sync():
# for i in range(10):
# task(i)
#
# def async():
# g_lst = []
# for i in range(10):
# g = gevent.spawn(task,i)
# g_lst.append(g)
# gevent.joinall(g_lst) # for g in g_lst:g.join()
# sync()
# async()


# 协程 : 能够在一个线程中实现并发效果的概念
# 能够规避一些任务中的IO操作
# 在任务的执行过程中,检测到IO就切换到其他任务

# 多线程 被弱化了
# 协程 在一个线程上 提高CPU 的利用率
# 协程相比于多线程的优势 切换的效率更快

# 爬虫的例子
# 请求过程中的IO等待
# from gevent import monkey;monkey.patch_all()
# import gevent
# from urllib.request import urlopen # 内置的模块
# def get_url(url):
# response = urlopen(url)
# content = response.read().decode('utf-8')
# return len(content)
#
# g1 = gevent.spawn(get_url,'http://www.baidu.com')
# g2 = gevent.spawn(get_url,'http://www.sogou.com')
# g3 = gevent.spawn(get_url,'http://www.taobao.com')
# g4 = gevent.spawn(get_url,'http://www.hao123.com')
# g5 = gevent.spawn(get_url,'http://www.cnblogs.com')
# gevent.joinall([g1,g2,g3,g4,g5])
# print(g1.value)
# print(g2.value)
# print(g3.value)
# print(g4.value)
# print(g5.value)

# ret = get_url('http://www.baidu.com')
# print(ret)

# socket server



#通过gevent实现单线程下的socket并发
#注意 :from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞

#server 端
# from gevent import monkey;monkey.patch_all()
# import socket
# import gevent
# def talk(conn):
# conn.send(b'hello')
# print(conn.recv(1024).decode('utf-8'))
# conn.close()
#
# sk = socket.socket()
# sk.bind(('127.0.0.1',8080))
# sk.listen()
# while True:
# conn,addr = sk.accept()
# gevent.spawn(talk,conn)
# sk.close()


#client 端
# import socket
# sk = socket.socket()
# sk.connect(('127.0.0.1',8080))
# print(sk.recv(1024))
# msg = input('>>>').encode('utf-8')
# sk.send(msg)
# sk.close()


# ===================== IO 模型

# IO模型

#阻塞 非阻塞 io多路复用

#阻塞
# 几乎所有的程序员第一次接触到的网络编程都是从listen()、send()、recv() 等接口开始的,
# 使用这些接口可以很方便的构建服务器/客户机的模型。然而大部分的socket接口都是阻塞型的
# 实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,
# 如在调用recv(1024)的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。


# 非阻塞
#在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有

# 所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
# 但是非阻塞IO模型绝不被推荐。
# 我们不能否则其优点:能够在等待任务完成的时间里干其他活了
# (包括提交其他任务,也就是 “后台” 可以有多个任务在“”同时“”执行)。
#但是也难掩其缺点:
#1. 循环调用recv()将大幅度推高CPU占用率;这也是我们在代码中留一句time.sleep(2)的原因,否则在低配主机下极容易出现卡机情况
#2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,
# 而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
# 非阻塞 代码 server 端
# import socket
# sk = socket.socket()
# sk.bind(('127.0.0.1',9000))
# sk.setblocking(False)
# sk.listen()
# conn_l = []
# del_conn = []
# while True:
# try:
# conn,addr = sk.accept() #不阻塞,但是没人连我会报错
# print('建立连接了:',addr)
# conn_l.append(conn)
# except BlockingIOError:
# for con in conn_l:
# try:
# msg = con.recv(1024) # 非阻塞,如果没有数据就报错
# if msg == b'':
# del_conn.append(con)
# continue
# print(msg)
# con.send(b'byebye')
# except BlockingIOError:pass
# for con in del_conn:
# con.close()
# conn_l.remove(con)
# del_conn.clear()
# while True : 10000 500 501

# 非阻塞 client 端
# import time
# import socket
# import threading
# def func():
# sk = socket.socket()
# sk.connect(('127.0.0.1',9000))
# sk.send(b'hello')
# time.sleep(1)
# print(sk.recv(1024))
# sk.close()
# for i in range(2):
# threading.Thread(target=func).start()


# IO 模型多虑复用
# 当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,
# 当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
# 强调:
# 1 。如果处理的连接数不是很高的话,使用select / epoll的web
# server不一定比使用multi - threading + blockingIO的web
# server性能更好,可能延迟还更大。select / epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
# 2 。 在多路复用模型中,对于每一个socket,一般都设置成为non - blocking,但是,如上图所示,
# 整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socketIO给block。
# 结论: select的优势在于可以处理多个连接,不适用于单个连接

#该模型的优点:
#相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,
# 不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。

#该模型的缺点:
#首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。
#很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。
#如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异,
#所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。
#其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。

# 代码 server 端
# import select
# import socket
#
# sk = socket.socket()
# sk.bind(('127.0.0.1',8000))
# sk.setblocking(False)
# sk.listen()
#
# read_lst = [sk]
# while True: # [sk,conn]
# r_lst,w_lst,x_lst = select.select(read_lst,[],[])
# for i in r_lst:
# if i is sk:
# conn,addr = i.accept()
# read_lst.append(conn)
# else:
# ret = i.recv(1024)
# if ret == b'':
# i.close()
# read_lst.remove(i)
# continue
# print(ret)
# i.send(b'goodbye!')


# client 端
# import time
# import socket
# import threading
# def func():
# sk = socket.socket()
# sk.connect(('127.0.0.1',8000))
# sk.send(b'hello')
# time.sleep(3)
# print(sk.recv(1024))
# sk.close()
#
# for i in range(20):
# threading.Thread(target=func).start()


# 异步IO(Asynchronous I/O)
#Linux下的asynchronous IO其实用得不多
#  用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,
# 当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,
# kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,
# 告诉它read操作完成了

# 代码
# import selectors
# from socket import *
#
# def accept(sk,mask):
# conn,addr=sk.accept()
# sel.register(conn,selectors.EVENT_READ,read)
#
# def read(conn,mask):
# try:
# data=conn.recv(1024)
# if not data:
# print('closing',conn)
# sel.unregister(conn)
# conn.close()
# return
# conn.send(data.upper()+b'_SB')
# except Exception:
# print('closing', conn)
# sel.unregister(conn)
# conn.close()
#
# sk=socket()
# sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
# sk.bind(('127.0.0.1',8088))
# sk.listen(5)
# sk.setblocking(False) #设置socket的接口为非阻塞
# sel=selectors.DefaultSelector() # 选择一个适合我的IO多路复用的机制
# sel.register(sk,selectors.EVENT_READ,accept)
#相当于网select的读列表里append了一个sk对象,并且绑定了一个回调函数accept
# 说白了就是 如果有人请求连接sk,就调用accrpt方法

# while True:
# events=sel.select() #检测所有的sk,conn,是否有完成wait data阶段
# for sel_obj,mask in events: # [conn]
# callback=sel_obj.data #callback=read
# callback(sel_obj.fileobj,mask) #read(conn,1)
posted @ 2018-04-03 22:17  xuerh  阅读(338)  评论(0编辑  收藏  举报