python 学习笔记九 队列,异步IO

queue (队列)

队列是为线程安全使用的。

1.先入先出

import queue
#测试定义类传入队列
class Foo(object):
    def __init__(self,n):
        self.n = n
new = queue.Queue(maxsize=3)
new.put(1)
new.put(Foo(1),timeout=2) # 超时时间后,抛出队列full异常
new.put([1, 2, 3],timeout=2)
print(new.full()) #判断队列是否满 True
#new.put("abc",timeout=1) #队列已满,再放报错
print(new.qsize()) # 查看当前队列长度
print(new.get())
print(new.get())
print(new.get())
print(new.empty()) #判断队列是否为空 True
#print(new.get_nowait()) #队列已空,取不到数据报异常

2.后进先出

q = queue.LifoQueue() #指定使用LifoQueue
q.put(3)
q.put(2)
print(q.get_nowait())
print(q.get_nowait())

“""
2
3
"""

3.优先级队列

存入一个元组,第一个为优先级,第二个为数据,第三个默认超时时间

import queue

new = queue.PriorityQueue(maxsize=3)
new.put((1,[1,2,3]))
new.put((10,"strings"))
new.put((20,"strings"))
print(new.get_nowait())
print(new.get_nowait())
print(new.get_nowait())

“”“
(1, [1, 2, 3])
(10, 'strings')
(20, 'strings')
“”“

生成者消费者模型

通过queue.task_done 和 queue.join 实现

一对一

import threading, queue, time
#生产者消费者模型为了程序松耦合,

#生产者生产消息 def consumer(n):
while True: print("33[32;1m consumer [%s] 33[0m get task: %s" % (n, q.get())) time.sleep(1) # 每秒吃一个 q.task_done() # q.get()1次通知队列减少1
#消费者消费消息 def producter(n): count
= 1 while True: print("producter [%s] produced a new task : %s" % (n, count)) q.put(count) count += 1 q.join() #消息阻塞 队列为空,重新触发 print("all task has been cosumed by consumers ...")
q = queue.Queue()
c1 = threading.Thread(target=consumer, args=[1, ])
p1 = threading.Thread(target=producter, args=["p1", ])
c1.start()
p1.start()

#result:
producter [p1] produced a new task : 1 #生产一个消息 consumer [1] get task: 1          #消费一个消息,q.task_done() 通知队列减少1个消息 all task has been cosumed by consumers ... #q.join() 收到队列为空,开始生产消息 producter [p1] produced a new task : 2 consumer [1] get task: 2 all task has been cosumed by consumers ... producter [p1] produced a new task : 3 consumer [1] get task: 3 all task has been cosumed by consumers ... producter [p1] produced a new task : 4 consumer [1] get task: 4 all task has been cosumed by consumers ... producter [p1] produced a new task : 5 consumer [1] get task: 5

一对多

def consumer(n):
while True:
print("33[32;1m consumer [%s] 33[0m get task: %s" % (n, q.get()))
time.sleep(1) # 每秒吃一个
q.task_done() # get()1次通知队列减少1

def producter(n):
count = 1
while True:
print("producter [%s] produced a new task : %s" % (n, count))
q.put(count)
count += 1
q.join() #消息阻塞 队列为空重新触发
print("all task has been cosumed by consumers ...")

q = queue.Queue()
c1 = threading.Thread(target=consumer, args=[1, ])
c2 = threading.Thread(target=consumer, args=[2, ])
c3 = threading.Thread(target=consumer, args=[3, ])

p1 = threading.Thread(target=producter, args=["p1", ])
c1.start()
c2.start()
c3.start()
p1.start()

result:

producter [p1] produced a new task : 1
 consumer [1]  get task: 1
all task has been cosumed by consumers ...
producter [p1] produced a new task : 2
 consumer [2]  get task: 2
all task has been cosumed by consumers ...
producter [p1] produced a new task : 3
 consumer [3]  get task: 3
all task has been cosumed by consumers ...
producter [p1] produced a new task : 4
 consumer [1]  get task: 4
all task has been cosumed by consumers ...
producter [p1] produced a new task : 5
 consumer [2]  get task: 5
all task has been cosumed by consumers ...
producter [p1] produced a new task : 6
 consumer [3]  get task: 6
all task has been cosumed by consumers ...
producter [p1] produced a new task : 7
 consumer [1]  get task: 7
all task has been cosumed by consumers ...
producter [p1] produced a new task : 8
 consumer [2]  get task: 8
all task has been cosumed by consumers ...
producter [p1] produced a new task : 9
 consumer [3]  get task: 9
all task has been cosumed by consumers ...
producter [p1] produced a new task : 10
 consumer [1]  get task: 10

多对多

def consumer(n):
    while True:
        print("33[32;1m consumer [%s] 33[0m get task: %s" % (n, q.get()))
        time.sleep(1)  # 每秒吃一个
        q.task_done()  # get()1次通知队列减少1

def producter(n):
    count = 1
    while True:
        print("producter [%s] produced a new task : %s" % (n, count))
        q.put(count)
        count += 1
        q.join()  #消息阻塞 队列为空重新触发
        print("all task has been cosumed by consumers ...")

q = queue.Queue()
c1 = threading.Thread(target=consumer, args=[1, ])
c2 = threading.Thread(target=consumer, args=[2, ])
c3 = threading.Thread(target=consumer, args=[3, ])
p1 = threading.Thread(target=producter, args=["p1", ])
p2 = threading.Thread(target=producter, args=["p2", ])
c1.start()
c2.start()
c3.start()
p1.start()
p2.start()
result:

producter [p1] produced a new task : 1
producter [p2] produced a new task : 1
 consumer [1]  get task: 1
 consumer [2]  get task: 1
all task has been cosumed by consumers ...
all task has been cosumed by consumers ...
producter [p1] produced a new task : 2
producter [p2] produced a new task : 2
 consumer [3]  get task: 2
 consumer [2]  get task: 2
all task has been cosumed by consumers ...
producter [p1] produced a new task : 3
 consumer [1]  get task: 3
all task has been cosumed by consumers ...
producter [p2] produced a new task : 3
 consumer [2]  get task: 3
all task has been cosumed by consumers ...
producter [p1] produced a new task : 4
 consumer [3]  get task: 4
all task has been cosumed by consumers ...
all task has been cosumed by consumers ...
producter [p1] produced a new task : 5
producter [p2] produced a new task : 4
 consumer [1]  get task: 5
 consumer [2]  get task: 4
all task has been cosumed by consumers ...
all task has been cosumed by consumers ...

协程

协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:

协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

协程的好处:

  • 无需线程上下文切换的开销
  • 无需原子操作锁定及同步的开销
  • 方便切换控制流,简化编程模型
  • 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

缺点:

  • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  • 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序(可以通过生产者消费者模型解决)

使用yield模拟协程

#使用yield实现在单线程的情况下实现并发运算的效果
import time

def consumer(name):
    print("%s 准备吃包子!"%name)
    while True:
        baozi = yield  #yield接收返回值
        print("包子[%s]来了,被[%s]吃了!" %(baozi,name)

def producer(name):
    c.__next__()
    d.__next__()
    print("开始生产包子!")
    for i in range(5):
        time.sleep(1)
        print("做了2个包子!")
        c.send(i)  #发送给yield
        d.send(i)

if __name__ == '__main__':
    c = consumer("c1")
    d = consumer("c2")
    p = producer()

result:

c1 准备吃包子! #实例化消费者,遇到yield函数冻结接收yield返回值
c2 准备吃包子!
开始生产包子!
做了2个包子!
包子[0]来了,被[c1]吃了! #函数继续执行
包子[0]来了,被[c2]吃了!
做了2个包子!
包子[1]来了,被[c1]吃了!
包子[1]来了,被[c2]吃了!
做了2个包子!
包子[2]来了,被[c1]吃了!
包子[2]来了,被[c2]吃了!
做了2个包子!
包子[3]来了,被[c1]吃了!
包子[3]来了,被[c2]吃了!
做了2个包子!
包子[4]来了,被[c1]吃了!
包子[4]来了,被[c2]吃了!

grentlet

from greenlet import greenlet

def test1():
    print(12)
    gr2.switch()  #手动切换
    print(34)
    gr2.switch()

def test2():
    print(56)
    gr1.switch()
    print(78)

gr1 = greenlet(test1) #加入协程
gr2 = greenlet(test2)
gr1.switch() 

"""
12
56
34
78
"""

Gevent 

import gevent

def foo():
    print('33[32;1m Running in foo33[0m')
    gevent.sleep(1)#阻塞1秒
    print('33[32;1m Explicit context switch to foo again33[0m')

def bar():
    print('33[31;1m Explicit context to bar33[0m')
    gevent.sleep(1)
    print('33[31;1m Implicit context switch back to bar33[0m')

def boom():
    print('33[33;1m just boom 33[0m')
    gevent.sleep(1)
    print('33[33;1m boom shakashaka 33[0m')


gevent.joinall(
    [   #将foo加入协程,协程间切换不会按照顺序而是随机切换
        gevent.spawn(foo),
        gevent.spawn(bar),
        gevent.spawn(boom)
    ]
)

result:

 Running in foo
 Explicit context to bar
 just boom
 Explicit context switch to foo again
 boom shakashaka
 Implicit context switch back to bar
"""

gevent实现遇到io阻塞自动切换

from gevent import monkey; monkey.patch_all()
import gevent
from  urllib.request import urlopen
 
def f(url):
    print('GET: %s' % url)
    resp = urlopen(url)
    data = resp.read()
    print('%d bytes received from %s.' % (len(data), url))
 
gevent.joinall([
        gevent.spawn(f, 'https://www.python.org/'),
        gevent.spawn(f, 'https://www.yahoo.com/'),
        gevent.spawn(f, 'https://github.com/'),
])

result:
"""
GET: https://www.python.org/
GET: https://www.yahoo.com/
GET: https://github.com/
46958 bytes received from https://www.python.org/.
24121 bytes received from https://github.com/.
413706 bytes received from https://www.yahoo.com/.
"""

gevent 实现多线程socketserver

import sys,time,gevent,socket
from gevent import socket, monkey
#将所有遇到的阻塞变为非阻塞
monkey.patch_all()

def server(port):
    s = socket.socket()
    s.bind(("0.0.0.0", port))
    s.listen(50)
    while True:
        cli, addr = s.accept()
        #派生协程 执行handle_request函数 将客户端socket连接传参
        gevent.spawn(handle_request, cli)
def handle_request(s):
    try:
        while True:
            data = s.recv(1024)
            print("recv:", data.decode("utf8"))
            s.send(data)
            if not data:
                #如果客户端断开连接此处没有效果,如果服务端断开连接,通知服务端去掉该连接
                s.shutdown(socket.SHUT_WR)
    except Exception as e:
        print(e)
    finally:
        s.close()

if __name__ == "__main__":
    server(8001)
import socket
 
HOST = 'localhost'    # The remote host
PORT = 8001           # The same port as used by the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
    msg = bytes(input(">>:"),encoding="utf8")
    s.sendall(msg)
    data = s.recv(1024)
    #print(data)
 
    print('Received', repr(data))
s.close()

select、poll、epoll

 select  

  select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,

  使得进程可以获得这些文件描述符从而进行后续的读写操作。

  select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。

  select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。

  另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。(解读:在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),

  让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。)

  同时,由于网络响应时间的延迟 使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

poll 

  poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。

  poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,

  它的开销随着文件描述符数量的增加而线性增大。另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()

  的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

epoll 

  直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

  epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,

  这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。

  epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表 就绪描述符数量的值,

  你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了 这些文件描述符在系统调用时复制的开销。

  另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描 述符进行扫描,而epoll事先通过epoll_ctl()

  来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调 机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

select实现多线程socketserver实例:

#server端
import select
import queue
import socket
import sys

#生成服务端socket实例
server = socket.socket()
#设置非阻塞 传入bool类型
server.setblocking(0)

#设置绑定的ip地址和端口
server_address = ('localhost', 10000)
print(sys.stderr, 'starting up on %s port %s' % server_address)
server.bind(server_address)

# 监听客户端最大连接数
server.listen(5)

#初始化读取数据的监听列表,最开始时希望从server这个套接字上读取数据
inputs = [server, ]

#初始化写入数据的监听列表,最开始并没有客户端连接进来,所以列表为空
outputs = []

#消息队列用字典表示 键为客户端socket对象,值为发送内容 可能有多个客户端连接,发送多条信息,将消息先存入队列而不是直接发送
message_queues = {}

#inputs列表默认存放server 即服务端的socket对象用于等待客户端接入
while inputs:
    print(sys.stderr, '
waiting for the next event')
    # 注:select能够监控 f=open(),obj=socket(),sys.stdin,sys.stdout终端输入输出(所有带fileno()方法的文件句柄)
    #文件操作是python无法检测的,windows也不支持终端输入输出的文件句柄(OSError:应用程序没有调用 WSAStartup,或者 WSAStartup 失败。)
    readable, writeable, exceptional = select.select(inputs, outputs, inputs)
    #一旦客户端连接,server的内容将改变,select检测到server的变化,将其返回给readable
    for s in readable:
        #默认只有server,等待客户端连接,但是有了client的socket对象后,等待的可能是客户端发送的消息,这里需要判断是socket还是消息
        if s is server:
            #创建客户端socket连接 connection 服务端为客户端生成的socket对象,client_address 客户端地址
            connection, client_address = s.accept()
            print(sys.stderr, 'new connection from', client_address)
            #客户端socket设置非阻塞
            connection.setblocking(0)
            # 因为有读操作发生,所以将此连接加入inputs
            inputs.append(connection)
            # 为每个连接创建一个queue队列,数据并不是立即发送需要放入队列,等待outputs队列有数据才发送,同时确保每个连接接收到正确的数据。
            message_queues[connection] = queue.Queue()
        #等待的将是客户端发送的数据
        else:
            #接收客户端数据
            data = s.recv(1024)
            if data:
                print(sys.stderr, 'received "%s" from %s' % (data, s.getpeername()))
                # 将收到的数据放入队列中
                message_queues[s].put(data)
                if s not in outputs:
                    # 将socket客户端的连接加入outputs中,并且用来给客户端返回数据。
                    outputs.append(s)
            else:#连接已经断开
                print(sys.stderr, 'closing', client_address, 'after reading no data')
                if s in outputs:
                    #因为连接已经断开,无需再返回消息,这时候如果这个客户端的连接对象还在outputs列表中,就把它删掉。
                    outputs.remove(s)
                #连接已经断开,在inputs中select也不用感知
                inputs.remove(s)
                #关闭会话
                s.close()
                #从字典中删除服务端为客户端建立连接的socket对象
                del message_queues[s]
    #一旦有参数,将一直为客户端返回数据
    for s in writeable:
        try:
            #读取客户端请求信息,采用非阻塞的方式get_nowait() 没有读取到数据抛出异常
            next_msg = message_queues[s].get_nowait()
        except queue.Empty:#引发队列空异常
            print(sys.stderr, 'output queue for', s.getpeername(), 'is empty')
            #没有读取到数据,无需为客户端返回消息,将其从outputs中删除,否则 select将一直感知,并传给writeable
            outputs.remove(s)
        else:#没有任何异常
            print(sys.stderr, 'sending "%s" to %s' % (next_msg, s.getpeername()))
            #此处是服务端原样返回接收到的信息
            s.send(next_msg)
    #如果服务端或客户端连接发生错误,exceptional将会有内容
    for s in exceptional:
        print(sys.stderr, 'handling exceptional condition for', s.getpeername())
        #将客户端连接删除
        inputs.remove(s)
        #如果还有数据未发完
        if s in outputs:
            #但是连接已经断开,只好从outputs删除
            outputs.remove(s)
        #关闭会话
        s.close()
        #删除该客户端连接队列,无须在发送数据了。
        del message_queues[s]
#client
import socket
import sys

#消息列表
messages = ['this is the message. ',
            'It will be sent ',
            'in parts.',
            ]
#ip_port
server_address = ('localhost', 10000)

#socket对象
socks = [socket.socket(socket.AF_INET, socket.SOCK_STREAM),
         socket.socket(socket.AF_INET,socket.SOCK_STREAM),
          ]

print(sys.stderr, 'connecting to %s port %s' % server_address)
#发起连接
for s in socks:
    s.connect(server_address)
#发送消息
for message in messages:
    for s in socks:
        print(sys.stderr, '%s: sending "%s"' % (s.getsockname(), message))
        #发送请求
        s.send(bytes(message, "utf8"))
    for s in socks:
        try:
            #接收信息
            data = s.recv(1024)
            print(sys.stderr, '%s: received "%s"' % (s.getsockname(), data))
        except Exception as e:
            print(e, 'closing socket', s.getsockname())
            #未收到回应,连接终止
            s.close()

更多详细内容:http://www.cnblogs.com/wupeiqi/articles/5040823.html

原文地址:https://www.cnblogs.com/koka24/p/5286061.html