python运维开发(十)----IO多路复用线程基本使用

内容目录:

  • python作用域
  • python2.7和python3.5的多继承区别
  • IO多路复用
  • socketserver模块源分析
  • 多线程、进程、协程

python作用域

 python中无块级作用域

if 1 == 1:
    name = 'jabe'

    print(name)
#可以正常输出jabe

#在python中无块级作用域
#在c#或者java中是不能这样使用的,提示name未定义的

python中以函数为作用域

def func():
    name = 'jbae'
func()
print(name)

#会提示name为定义的报错,说明在python中以函数为作用域的

 python作用域链由内想外找,直到找不到报错

name = 'jabe'
def f1():
    # name = 'a'
    def f2():
        # name = 'b'
        print(name)
    f2()
f1()

#输出
jabe

函数执行前,作用域(链)已经确定

name = 'jabe'

def f1():
    print(name)

def f2():
    name = 'ljb'
    f1()

f2()
输出:
jabe
#===============================
name = 'jabe'

def f1():
    print(name)

def f2():
    name = 'ljb'
    return f1

ret = f2()
ret()
输出:
jabe  

lambda函数作用域

li = [lambda :x for x in range(10)]
r = li[0]()
print(r)
print('===================')
#输出为9,上面的例子等同于下面的代码

li = []
for i in range(10):
    def f1():
        return i
    li.append(f1)
#li 为列表,内部的元素为相同功能的函数

print(li[0]()) print(li[1]()) print(li[2]()) print(li[3]()) #输出: # 9 # =================== # 9 # 9 # 9 # 9

由于函数为被调用时是未被执行的,所以当循环结束时i为9,最后调用时候显示均为9

上面的函数我们稍作更改就可以打印的不为9了

li = []
for i in range(10):
    def f1(x=i):
        return x
    li.append(f1)

#li 为列表,内部的元素为相同功能的函数

print(li[0]())
print(li[1]())
print(li[2]())
print(li[3]())

#输出:
# 0
# 1
# 2
# 3

上面的例子中因为每次执行循环,将i值赋给x,因此打印的为0,1,2,3

python2.7和python3.5的多继承区别

我们从以前学过的知识中可以知道,python3.5类的多重继承顺序为从左到右,由下到上的顺序来继承寻找的。

在python2.7中,如果为新式类和py3.5中是寻找的顺序是一样的,但是经典类的继承顺序稍有不同,一直深层寻找,直到找到为止,不会向右边寻找。

不继承object是经典类,继承object是新式类 (和py3中一致)

IO多路复用

 I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

python中有一个select模块,提供了select,poll,epoll三个方法

select:适用多平台,win+linux都可以,inputs 最多监控1024个对象,底层for 循环,效率不高

poll:适用多平台,inputs最多监控对象个数无限制,底层和select 一样都为for循环

epoll:适用于linux平台,底层采用触发机制,发生变化时通知,非循环遍历,效率要比select和poll要高很多

 select方法说明:

句柄列表11, 句柄列表22, 句柄列表33 = select.select(句柄序列1, 句柄序列2, 句柄序列3, 超时时间)
 
参数: 可接受四个参数(前三个必须)
返回值:三个列表
 
select方法用来监视文件句柄,如果句柄发生变化,则获取该句柄。
1、当 参数1 序列中的句柄发生可读时(accetp和read),则获取发生变化的句柄并添加到 返回值1 序列中
2、当 参数2 序列中含有句柄时,则将该序列中所有的句柄添加到 返回值2 序列中
3、当 参数3 序列中的句柄发生错误时,则将该发生错误的句柄添加到 返回值3 序列中
4、当 超时时间 未设置,则select会一直阻塞,直到监听的句柄发生变化
   当 超时时间 = 1时,那么如果监听的句柄均无任何变化,则select会阻塞 1 秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。

利用select 实现伪并发,同时处理多个客户端的请求:服务端

import socket
import select

sk = socket.socket()
sk.bind(('127.0.0.1', 9999,))
sk.listen(5)
inputs = [sk,]
outputs = []
messages = {}


while True:
    rlist, wlist, elist = select.select(inputs,outputs,[sk],1)
    print(len(inputs),len(rlist),len(outputs),len(wlist))
    # 监听sk(服务器端)对象,如果sk对象发生变化,表示有客户端来连接了,此时rlist值为【sk】
    # 监听conn对象,如果conn发生变化,表示客户端有新消息发送过来了,此时rlist的值为 【客户端】
    for r in rlist:
        if r == sk:# 新客户来连接
            conn,address = r.accept()# conn是什么?其实socket对象
            inputs.append(conn)
            messages[conn] = []
            conn.sendall(bytes('Hello',encoding='utf-8'))
        else: # 有人给我发消息了
            print('====================')
            try:
                ret = r.recv(1024)
                # r.sendall(ret)
                if not ret:
                    raise Exception('断开连接')
                else:
                    outputs.append(r)
                    messages[r].append(ret)
            except Exception as e:
                inputs.remove(r)
                del messages[r]
    
    for w in wlist:# 所有给我发过消息的人
        msg = messages[w].pop()
        resp = msg + bytes('response', encoding='utf-8')
        w.sendall(resp)
        outputs.remove(w)

利用select 实现伪并发,同时处理多个客户端的请求:客户端  

import socket
sk = socket.socket()
ip_port = ('127.0.0.1',9999,)
sk.connect(ip_port)
data = sk.recv(1024)
print(data.decode())
while True:
    inp = input(">>>")
    sk.sendall(bytes(inp,encoding='utf-8'))
    print(sk.recv(1024).decode())

sk.close()

通过上面的实例我们可以知道通过IO多路复用可以实现读写的分离,其实IO多路复用,并不是真正的多并发处理,只是底层循环接收消息,接收到进行处理的,不是同时接收多并发处理的,所以上面的例子称之为伪并发。真正的socketserver实现的了真正的并发,其他socketserver中还用的多线程、进程的使用,所以我们也可以通过多线程方式加上这种伪并发来实现socketserver的多并发的功能。

socketserver模块源分析

SocketServer内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求。

源码分析

我们通过一个简单例子来分析执行过程:

import socketserver

class MyClass(socketserver.BaseRequestHandler):

    def handle(self):
        pass

obj = socketserver.ThreadingTCPServer(('127.0.0.1', 9999), MyClass)
obj.serve_forever() 

socketserver 调用过程如下图:

内部调用流程为:

  • 启动服务端程序
  • 执行 TCPServer.__init__ 方法,创建服务端Socket对象并绑定 IP 和 端口
  • 执行 BaseServer.__init__ 方法,将自定义的继承自SocketServer.BaseRequestHandler 的类 MyRequestHandle赋值给self.RequestHandlerClass
  • 执行 BaseServer.server_forever 方法,While 循环一直监听是否有客户端请求到达 ...
  • 当客户端连接到达服务器
  • 执行 ThreadingMixIn.process_request 方法,创建一个 “线程” 用来处理请求
  • 执行 ThreadingMixIn.process_request_thread 方法
  • 执行 BaseServer.finish_request 方法,执行 self.RequestHandlerClass()  即:执行 自定义 MyRequestHandler 的构造方法(自动调用基类BaseRequestHandler的构造方法,在该构造方法中又会调用 MyRequestHandler的handle方法)

多线程、进程、协程

通过前面的学习我们可以知道,创建socket对象方式进行通讯默认是单进程单线程工作方式,而socketserver是通过多进程多线程的方式,在多线程中我们引用threding的模块。

threading用于提供线程相关的操作,线程是应用程序中工作的最小单元。

thread简单实例:

import threading
import  time

def f1(arg):
    time.sleep(2)
    print(arg)

for i in range(10):
    t1 = threading.Thread(target=f1,args=(i,))
    t1.start()

上述代码创建了10个“前台”线程,然后控制器就交给了CPU,CPU根据指定算法进行调度,分片执行指令。  

thread的其他方法:

  • start            线程准备就绪,等待CPU调度
  • setName      为线程设置名称
  • getName      获取线程名称
  • setDaemon   设置为后台线程或前台线程(默认)
  •                    如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止
  •                     如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止
  • join              逐个执行每个线程,执行完毕后继续往下执行,该方法使得多线程变得无意义
  • run              线程被cpu调度后自动执行线程对象的run方法

 

import threading
import  time

def f1(arg):
    time.sleep(2)
    print(arg)

# for i in range(10):
#     t1 = threading.Thread(target=f1,args=(i,))
#     t1.start()



t1 = threading.Thread(target=f1,args=(123,))
t1.setDaemon(True) #默认为False,设置为True表示主线程不等子线程
t1.start() #不代表当前线程会被立即执行
t1.join(1) #表示主线程到此等待。。。直到子线程执行完毕,参数表示主线程最多等待n秒

print('end')

 

posted @ 2016-07-11 19:36  Jabe  阅读(405)  评论(0)    收藏  举报