循序渐进Python3(八) -- 1 -- socket进阶

IO多路复用

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


Linux中的 select,poll,epoll 都是IO多路复用的机制。

select
 
select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另 外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量 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()时便得到通知。


Python中有一个select模块,其中提供了:select、poll、epoll三个方法,分别调用系统的 select,poll,epoll 从而实现IO多路复用。

1
2
3
4
5
6
Windows Python:
    提供: select
Mac Python:
    提供: select
Linux Python:
    提供: select、poll、epoll
注意:网络操作、文件操作、终端操作等均属于IO操作,对于windows只支持Socket操作,其他系统支持其他IO操作,但是无法检测 普通文件操作 自动上次读取是否已经变化。

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 秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。

例子:
服务端:
#!/usr/bin/env python
# Version = 3.5.2
# __auth__ = '无名小妖'
import socket, select

sk = socket.socket()
sk.bind(('127.0.0.1',9992))
sk.listen(5)

inputs = [sk, ]
while True:
# 监听sk对象,如果sk发生变化,表示有客户端来连接了,此时rlist的值为sk
rlist,w,e = select.select(inputs,[],[],1)
# 监听conn对象,如果conn变化,表示客户端来发消息了,此时的rlist值为 客户端
print(len(inputs), len(rlist))
for r in rlist:
if r == sk: # 新连接
conn, addr = r.accept()
inputs.append(conn)
conn.sendall(bytes('hello !', encoding='utf8'))
else: # 有人发消息
r.recv(1024)
客户端:
#!/usr/bin/env python
# Version = 3.5.2
# __auth__ = '无名小妖'
import socket

client = socket.socket()
client.connect(('127.0.0.1',9992))

data = client.recv(1024)
print(data.decode())
while True:
inp = input('>>>')
client.sendall(bytes(inp, encoding='utf8'))
print(client.recv(1024).decode())
client.close()
 

完整例子:
服务端:
#!/usr/bin/env python
# Version = 3.5.2
# __auth__ = '无名小妖'
import socket, select

sk = socket.socket()
sk.bind(('127.0.0.1',9992))
sk.listen(5)

inputs = [sk, ]
outputs = []
while True:
# 监听sk对象,如果sk发生变化,表示有客户端来连接了,此时rlist的值为sk
rlist,wlist,e = select.select(inputs, outputs, [], 1)
# 监听conn对象,如果conn变化,表示客户端来发消息了,此时的rlist值为 客户端
print(len(inputs), len(rlist), len(outputs), len(wlist))
for r in rlist:
if r == sk: # 新连接
conn, addr = r.accept()
inputs.append(conn)
conn.sendall(bytes('hello !', encoding='utf8'))
else: # 有人发消息
print('=============')
try:
ret = r.recv(1024)
if not ret:
raise Exception('断开连接!')
else:
outputs.append(r)
except Exception as e:
inputs.remove(r)
for w in wlist:
w.sendall(bytes('respons ',encoding='utf8'))
outputs.remove(w)
客户端:
#!/usr/bin/env python
# Version = 3.5.2
# __auth__ = '无名小妖'
import socket

client = socket.socket()
client.connect(('127.0.0.1',9992))

data = client.recv(1024)
print(data.decode())
while True:
inp = input('>>>')
client.sendall(bytes(inp, encoding='utf8'))
print(client.recv(1024).decode())
client.close()
-----------------------------------------------------------------------------------------

多线程 多进程

一个应用程序,可以有多个进程、多个线程,默认是单进程单线程。
python中由于有GIL(全局解释器锁)的存在,所以每次只能使用某个进程中的一个线程。
但是上述规则只限于使用cpu的时候,如果不实用cpu,那么可以使用多线程。
因此,在python中想提高并发有如下做法:
  io密集型:可以使用多线程(因为io操作不实用cpu)
  计算密集型:使用多进程

列子:
import time
def f1(arg, t=None):
if t:
t._delete()
time.sleep(5)
print(arg)


# for i in range(10):
# f1(i)
# 单进程、单线程的应用程序
import threading
t1 = threading.Thread(target=f1, args=(1,))
# t1.setDaemon(True) # true,表示主线程不等此子线程
t1.start()# 不代表当前线程会被立即执行
#t.join(2) # 表示主线程到此,等待 ... 直到子线程执行完毕
# 参数,表示主线程在此最多等待n

t2 = threading.Thread(target=f1, args=(2,t1))
t2.start()# 不代表当前线程会被立即执行
print('end')
print('end')
print('end')
print('end')
print('end')

创建线程的两种方式:

第一种:(常用的)
import threading
def f1(arg):
print(arg)

t = threading.Thread(target=f1, args=(123,))
t.start()
t.run()
run

第二种:
import threading
class MyThread(threading.Thread):
def __init__(self, func,args):
self.func = func
self.args = args
super(MyThread, self).__init__()

def run(self):
self.func(self.args)

def f2(arg):
print(arg)

obj = MyThread(f2,123)
obj.start()

----------------------------------------------------------------------------------

队列

python队列是创建在内存的,当程序退出,队列同时清空。
import queue
# queue.Queue,先进先出队列
# queue.LifoQueue,后进先出队列
# queue.PriorityQueue,优先级队列
# queue.deque,双向对队

先进先出队列

#!/usr/bin/env python
# Version = 3.5.2
# __auth__ = '无名小妖'
import queue

q = queue.Queue(2) # 队列最大长度
q.put(11) # put 放数据
q.put(22)
print(q.qsize()) # 查看队列当前元素个数
q.put(33, timeout=2) # timeout 等待时间,2
q.put(33, block=False) # block 是否阻塞,False表示不阻塞

print(q.get()) # get 取数据,默认阻塞
print(q.empty()) # 检查队列是否为空,返回TrueFalse
# maxsize 最大支持的个数

# join,task_done,阻塞进程,当队列中任务执行完毕之后,不再阻塞
import queue
q = queue.Queue()
q.put(123)
q.put(123)
q.get()
q.task_done() # 告诉队列取完了
q.get()
q.task_done()
q.join() # 结束
----------------------------------------------------------------------------------

生产者消费者模型

产生原因:提高应对并发的能力

线程

Threading用于提供线程相关的操作,线程是应用程序中工作的最小单元。
小例子:
import threading
import time
  
def show(arg):
    time.sleep(1)
    print('thread'+str(arg))

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

print('main thread stop')

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

更多方法:

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

自定义线程类

import threading import time class MyThread(threading.Thread): def __init__(self,num): threading.Thread.__init__(self) self.num = num def run(self):#定义每个线程要运行的函数 print("running on number:%s" %self.num) time.sleep(3) if __name__ == '__main__': t1 = MyThread(1) t2 = MyThread(2) t1.start() t2.start()

线程锁:

由于线程之间是进行随机调度,并且每个线程可能只执行n条,当多个线程同时修改同一条数据时可能会出现脏数据,所以,出现了线程锁 - 同一时刻只允许一个线程操作。
上面线程的第一个列子就未使用锁。
import threading import time NUM = 10 def func(i,l): global NUM # 上锁 l.acquire() # 30,5 25m5,20 NUM -= 1 time.sleep(2) print(NUM,i) # 开锁 l.release() # lock = threading.Lock() # lock = threading.RLock() # 支持多层锁 lock = threading.BoundedSemaphore(5) for i in range(30): t = threading.Thread(target=func,args=(i,lock,)) t.start()


信号量(Semaphore)

互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。

import threading,time

 

def run(n):

    semaphore.acquire()

    time.sleep(1)

    print("run the thread: %s" %n)

    semaphore.release()

 

if __name__ == '__main__':


    num= 0

    semaphore  = threading.BoundedSemaphore(5) #最多允许5个线程同时运行

    for i in range(20):

        t = threading.Thread(target=run,args=(i,))

        t.start()


事件(event)

 python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法 set、wait、clear。 事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。 clear:将“Flag”设置为False set:将“Flag”设置为True

列子:
import threading 
def func(i,e): 
    print(i) e.wait() # 检测是什么等,如果是红灯,停;绿灯,行 
    print(i+100) event = threading.Event() 
    for i in range(10): 
        t = threading.Thread(target=func, args=(i,event,)) 
        t.start() 
#======== 
event.clear() # 设置成红灯 
inp = input('>>>') 
if inp == "1": 
    event.set() # 设置成绿灯


条件(Condition)

使得线程等待,只有满足某条件时,才释放n个线程

例子一:

import threading

def func(i,con):

    print(i)

    con.acquire()

    con.wait()

    print(i+100)

    con.release()


c = threading.Condition()

for i in range(10):

    t = threading.Thread(target=func, args=(i,c,))

    t.start()


while True:

    inp = input('>>>')

    if inp == 'q':

        break

    #  以下 为 条件 代码,inp为几 上面线程就会执行几个

    c.acquire()

    c.notify(int(inp))

    c.release()


例子二:

import threading # 输入true则执行一个线程 def condition(): ret = False r = input('>>>') if r == 'true': ret = True else: ret = False return ret def func(i,con): print(i) con.acquire() con.wait_for(condition) print(i+100) con.release() c = threading.Condition() for i in range(10): t = threading.Thread(target=func, args=(i,c,)) t.start()


定时器(Timer)

定时器,指定n秒后执行某操作

from threading import Timer


def hello():

    print("hello, world")


t = Timer(1, hello)

t.start()  # after 1 seconds, "hello, world" will be printed


线程池

什么是线程池? 

诸如web服务器、数据库服务器、文件服务器和邮件服务器等许多服务器应用都面向处理来自某些远程来源的大量短小的任务。 构建服务器应用程序的一个过于简单的模型是:每当一个请求到达就创建一个新的服务对象,然后在新的服务对象中为请求服务。 但当有大量请求并发访问时,服务器不断的创建和销毁对象的开销很大。 所以提高服务器效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这样就引入了“池”的概念, “池”的概念使得人们可以定制一定量的资源,然后对这些资源进行复用,而不是频繁的创建和销毁。

 线程池是预先创建线程的一种技术。 这些线程都是处于睡眠状态,即均为启动,不消耗CPU,而只是占用较小的内存空间。 当请求到来之后,缓冲池给这次请求分配一个空闲线程,把请求传入此线程中运行,进行处理。 当预先创建的线程都处于运行状态,即预制线程不够,线程池可以自由创建一定数量的新线程,用于处理更多的请求。 当系统比较闲的时候,也可以通过移除一部分一直处于停用状态的线程。

 线程池的注意事项 

虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。在使用线程池时需注意线程池大小与性能的关系,注意并发风险、死锁、资源不足和线程泄漏等问题。

 1、线程池大小。多线程应用并非线程越多越好,需要根据系统运行的软硬件环境以及应用本身的特点决定线程池的大小。 一般来说,如果代码结构合理的话,线程数目与CPU 数量相适合即可。 如果线程运行时可能出现阻塞现象,可相应增加池的大小;如有必要可采用自适应算法来动态调整线程池的大小,以提高CPU 的有效利用率和系统的整体性能。 

2、并发错误。多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意避免死锁现象的发生。

 3、线程泄漏。这是线程池应用中一个严重的问题,当任务执行完毕而线程没能返回池中就会发生线程泄漏现象。 

线程池要点: 

 1、通过判断等待的任务数量和线程池中的最大值,取最小值来判断开启多少线程来工作 比如: 任务数是3,进程池最大20 ,那么咱们只需要开启3个线程就行了。 任务数是500,进程池是20,那么咱们只开20个线程就可以了。 取最小值 

 2、实现线程池正在运行,有一个查看的功能,查看一下现在线程里面活跃的线程是多少等待的是多少? 线程总共是多少,等待中多少,正在运行中多少 作用: 方便查看当前线程池状态 能获取到这个之后就可以当线程一直处于空闲状态 查看状态用:上下文管理来做,非常nice的一点

 3、关闭线程


简单线程池的实现:

import queue import threading import time class ThreadPool: def __init__(self, maxsize=5): self.maxsize = maxsize self._q = queue.Queue(maxsize) for i in range(maxsize): self._q.put(threading.Thread)
def get_thread(self): return self._q.get() def add_thread(self): self._q.put(threading.Thread) pool = ThreadPool(5) def task(arg,p): print(arg) time.sleep(1) p.add_thread() for i in range(100): # threading.Thread类 t = pool.get_thread() obj = t(target=task,args=(i,pool,)) obj.start()

--------------------------------------------------------------------------------------------

Python 进程

注意:由于进程之间的数据需要各自持有一份,所以创建进程需要非常大的开销。

      进程各自持有一份数据,默认无法共享数据。


进程使用:

from multiprocessing import Process from multiprocessing import queues import multiprocessing from multiprocessing import Array def foo(i,arg): # arg.put(i) # print('say hi',i,arg.qsize()) arg[i] = i + 100 for item in arg: print(item) print('================') if __name__ == "__main__": # li = [] # li = queues.Queue(20,ctx=multiprocessing) li = Array('i', 10) for i in range(10): p = Process(target=foo,args=(i,li,)) #p.daemon = True p.start() #p.join()


实现数据共享的方式:

queues,array,Manager.dict


数据共享:(queues)
from multiprocessing import Process
from multiprocessing import queues
import multiprocessing
def foo(i,arg):
arg.put(i)
print('say hi',i,arg.qsize())

if __name__ == "__main__":
# li = []
li = queues.Queue(20,ctx=multiprocessing) # 实现数据共享queues 和 multiprocessing
for i in range(10):
p = Process(target=foo,args=(i,li,))
#p.daemon = True
p.start()
#p.join()

数据共享:(array)
from multiprocessing import Process
from multiprocessing import Array # 数组,类似列表

def foo(i,arg):
arg[i] = i + 100
for item in arg:
print(item)
print('================')
if __name__ == "__main__":
li = Array('i', 10)
for i in range(10):
p = Process(target=foo,args=(i,li,))
p.start()
数据共享:(Manager
from multiprocessing import Process
from multiprocessing import Manager

def foo(i,arg):
arg[i] = i + 100
print(arg.values())
if __name__ == "__main__":
obj = Manager()
li = obj.dict()
for i in range(10):
p = Process(target=foo,args=(i,li,))
p.start()
p.join() # 方式二
# 方式一
# import time
# time.sleep(0.1)

进程锁:

没锁:
from multiprocessing import Process
from multiprocessing import Array
import time

def foo(i,lis):
lis[0] = lis[0] - 1
time.sleep(1)
print('say hi',lis[0])
if __name__ == "__main__":
li = Array('i', 1)
li[0] = 10
for i in range(10):
p = Process(target=foo,args=(i,li))
p.start()
有锁:
from multiprocessing import Process
from multiprocessing import Array
from multiprocessing import RLock
import time
def foo(i,lis,lc):
lc.acquire()
lis[0] = lis[0] - 1
time.sleep(1)
print('say hi',lis[0])
lc.release()
if __name__ == "__main__":
# li = []
li = Array('i', 1)
li[0] = 10
lock = RLock()
for i in range(10):
p = Process(target=foo,args=(i,li,lock))
p.start()

进程池:

进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。

进程池中有两个方法:

apply

apply_async


    例子:
    from multiprocessing import Pool
    import time
    def f1(arg):
    print(arg,'b')
    time.sleep(5)
    print(arg,'a')
    if __name__ == "__main__":
    pool = Pool(5)
    for i in range(30):
    # pool.apply(func=f1,args=(i,))
    pool.apply_async(func=f1,args=(i,))
    # pool.close() # 所有的任务执行完毕
    time.sleep(1)
    pool.terminate() # 立即终止,当前已经执行的任务完毕
    pool.join() # 夯住,前面必须执行close或者terminate方法
    pass
    -----------------------------------------------------------------------------------------------------------------

    协程:

    线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作则是程序员。

    协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序。

    协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程;

    先看一下基本用法:
    greenlet:

    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()

    gevent:

    import gevent
    def foo():
    print('Running in foo')
    gevent.sleep(0)
    print('Explicit context switch to foo again')
    def bar():
    print('Explicit context to bar')
    gevent.sleep(0)
    print('Implicit context switch back to bar')
    gevent.joinall([
    gevent.spawn(foo),
    gevent.spawn(bar),
    ])

    简单的例子:
    from gevent import monkey; monkey.patch_all()
    import gevent
    import requests
    def f(url):
    print('GET: %s' % url)
    resp = requests.get(url)
    data = resp.text
    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/'),
    ])

    返回结果:
    GET: https://www.python.org/ GET: https://www.yahoo.com/ GET: https://github.com/ 47433 bytes received from https://www.python.org/. 25751 bytes received from https://github.com/. 462131 bytes received from https://www.yahoo.com/.





    原文地址:https://www.cnblogs.com/wumingxiaoyao/p/5942842.html