并发编程
进程
顾名思义,进程即正在执行的一个过程。进程是对正在运行程序的一个抽象。
进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一。操作系统的其他所有内容都是围绕进程的概念展开的
必备的理论基础
#一 操作系统的作用: 1:隐藏丑陋复杂的硬件接口,提供良好的抽象接口 2:管理、调度进程,并且将多个进程对硬件的竞争变得有序 #二 多道技术: 1.产生背景:针对单核,实现并发 ps: 现在的主机一般是多核,那么每个核都会利用多道技术 有4个cpu,运行于cpu1的某个程序遇到io阻塞,会等到io结束再重新调度,会被调度到4个 cpu中的任意一个,具体由操作系统调度算法决定。 2.空间上的复用:如内存中同时有多道程序 3.时间上的复用:复用一个cpu的时间片 强调:遇到io切,占用cpu时间过长也切,核心在于切之前将进程的状态保存下来,这样 才能保证下次切换回来时,能基于上次切走的位置继续运行
并行与并发
并发:是伪并行,即看起来是同时运行。单个cpu+多道技术就可以实现并发,(并行也属于并发)
并行:同时运行,只有具备多个cpu才能实现并行
IO阻塞
从磁盘读取数据是有延迟的,磁头寻道的平均延时为5毫秒,在磁道上找到数据的平均延时为4毫秒,在IO阻塞的时间里,cpu就不会继续工作,这是操作系统就会把cpu分配给其它程序
使用multiprocessing模块开启进程
from multiprocessing import Process import time # win 创建进程接口 CreateProcess linux为fork # linux系统子进程的初始状态和父进程完全一样,win上不完全一样 def task(name): print('%s is running'%name) time.sleep(2) print('%s is done' % name) if __name__ == '__main__': # Process(target=task,kwargs={'name':'egon'}) p = Process(target=task,args=('egon',)) p.start() print('主')
开启进程的两种方式
# 方式一 from multiprocessing import Process import time def task(name): print('%s is running'%name) time.sleep(2) print('%s is done' % name) if __name__ == '__main__': p = Process(target=task,args=('egon',)) p.start() print('主') # 方式二 from multiprocessing import Process import time class Myprocess(Process): def __init__(self,name): super().__init__() self.name = name def run(self): print('%s is running' % self.name) time.sleep(2) print('%s is done' % self.name) if __name__ == '__main__': p = Myprocess('egon') p.start() print('主')
Process对象的属性和方法
# 运行一个python文件其实是用python解释器去执行 # import time,os # print(os.getpid(),os.getppid()) # pid当前进程的进程号,ppid父进程的进程号 # time.sleep() from multiprocessing import Process import time def task(name): print('%s is running'%name) time.sleep(2) print('%s is done' % name) if __name__ == '__main__': p = Process(target=task,args=('egon',),name='xxx') p.start() # print(p.name) # 打印进程的名字 # print(p.pid) # 进程号 # print(p.is_alive()) # 是否存活 # p.terminate() # 杀死进程,向操作系统发出指令,不要用 # p.join() # join(p) 等待p执行完 p.join(timeout=2) 可以指定超时时间,一般不用 print('主')
进程池
在利用Python进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间。多进程是实现并发的手段之一,需要注意的问题是:
- 很明显需要并发执行的任务通常要远大于核数
- 一个操作系统不可能无限开启进程,通常有几个核就开几个进程
- 进程开启过多,效率反而会下降(开启进程是需要占用系统资源的,而且开启多余核数目的进程也无法做到并行)
例如当被操作对象数目不大时,可以直接利用multiprocessing中的Process动态成生多个进程,十几个还好,但如果是上百个,上千个。。。手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。
我们就可以通过维护一个进程池来控制进程数目,比如httpd的进程模式,规定最小进程数和最大进程数
创建进程池的类:如果指定numprocess为3,则进程池会从无到有创建三个进程,然后自始至终使用这三个进程去执行所有任务,不会开启其他进程
''' 提交/调用任务的方式有两种: 同步调用:提交/调用一个任务,然后就在原地等着,等到该任务执行完毕拿到结果,再执行下一行代码 异步调用:提交/调用一个任务,不在原地等着,直接执行下一行代码,结果 ''' # from multiprocessing import Process,Pool from concurrent.futures import ProcessPoolExecutor import time,random,os def piao(name): print('%s is piaoing %s'%(name,os.getpid())) time.sleep(random.randint(1,3)) if __name__ == '__main__': p = ProcessPoolExecutor(4) # 从始至终只有4个进程干活,进程号不变 objs = [] for i in range(10): obj = p.submit(piao,'alex%s'%i) # obj.result() # 进程运行的结果 # res = p.submit(piao,'alex%s'%i).result() # 同步调用,要等待任务的执行结果 # obj = p.submit(piao, 'alex%s'%i) # 异步调用 objs.append(obj) for i in objs: print(i.result()) p.shutdown(wait=True) # 等待进程池中的任务都干完,同时禁止往池中添加任务了 # 使用Pool时,分两步 pool.close() pool.join() print('主',os.getpid())
线程
在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程
线程顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程是一个进程
车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一个流水线
流水线的工作需要电源,电源就相当于cpu
所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。
例如,北京地铁与上海地铁是不同的进程,而北京地铁里的13号线是一个线程,北京地铁所有的线路共享北京地铁所有的资源,比如所有的乘客可以被所有线路拉。
开启线程的两种方式
from threading import Thread import time,os # 第一种方式 def task(): print('%s is running' %os.getpid()) time.sleep(2) print('%s is done' %os.getpid()) if __name__ == '__main__': t = Thread(target=task,) t.start() print('主') ''' 1.一个子进程内不开进程也不开子线程:主线程结束,该进程就结束 2.当一个进程内开启子进程时: 主线程结束,主进程要等,等所有子进程运行完毕给儿子收尸 3.当一个进程内开启多个线程时: 主线程结束并不意味着进程结束, 进程的结束指的是该进程内所有的线程都运行完毕,才应该回收进程 ''' # 第二种方式 class Mythread(Thread): def __init__(self): super().__init__() def run(self): print('%s is running' % os.getpid()) time.sleep(2) print('%s is done' % os.getpid()) if __name__ == '__main__': t = Mythread() t.start() print('主')
多线程指的是,在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。详细的讲分为4点:
1. 多线程共享一个进程的地址空间
2. 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
3. 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。
4. 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)
线程对象的其它属性和方法
from threading import Thread,current_thread,enumerate,active_count import time,os def task(): print('%s is running' %current_thread().getName()) # 线程名 current_thread()当前线程 time.sleep(2) print('%s is done' %os.getpid()) if __name__ == '__main__': # t = Thread(target=task,name='xxx') t = Thread(target=task) t.start() # t.join() # print(t.name) # 线程名 Thread-1 print(enumerate()) # 当前或者的线程对象 print(active_count()) # 当前活着线程的线程数 print('主',current_thread().getName()) # MainThread
线程与进程内存空间占用
# 进程之间内存空间隔离 from multiprocessing import Process n = 100 def task(): global n n = 0 if __name__ == '__main__': t = Process(target=task,) t.start() t.join() print('主',n) # 100 # 线程之间内存空间共享 from threading import Thread n = 100 def task(): global n n = 0 if __name__ == '__main__': t = Thread(target=task,) t.start() t.join() print('主',n) # 0
线程池
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor from threading import current_thread import time,random,os def task(n): print('%s is running'%current_thread().getName()) time.sleep(random.randint(1,3)) return n**2 if __name__ == '__main__': # t = ProcessPoolExecutor() # 默认是cpu的核数 # print(os.cpu_count()) # 查看cpu核数 t = ThreadPoolExecutor(3) # 默认为cpu的核数*5 objs = [] for i in range(10): obj = t.submit(task,i) objs.append(obj) t.shutdown(wait=True) for obj in objs: print(obj.result()) print('主',current_thread().getName())
异步调用和回调函数
import requests from threading import Thread,current_thread from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor import time def get(url): print('%s GET %s'%(current_thread().getName(),url)) response = requests.get(url) if response.status_code == 200: return {'url':url,'text':response.text} # print(type(response.text)) # <class 'str'> def parse(obj): res = obj.result() print('[%s] <%s> (%s)' % (current_thread().getName(), res['url'],len(res['text']))) # print('[%s] parse res [%s]'%(res['url'],len(res['text']))) if __name__ == '__main__': urls = [ 'https://www.python.org', 'https://www.baidu.com', 'https://www.jd.com' ] t = ThreadPoolExecutor(2) # t = ProcessPoolExecutor(2) for url in urls: t.submit(get,url).add_done_callback(parse) # parse(obj) t.shutdown(wait=True) print('主') ''' 异步调用: 提交完任务(为该任务绑定一个回调函数),不用在原地等任务执行完毕拿到结果,可以直接提交下一个任务 一个任务一旦执行完毕就会自动触发回调函数的运行 回调函数的参数是单一的: 回调函数的参数就是它所绑定任务的返回值 ''' # 进程池,回调的活由主进程干 # 线程池,回调的活都有可能干
守护进程
主进程创建守护进程
其一:守护进程会在主进程代码执行结束后就终止
其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children
注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止
# 守护进程 from multiprocessing import Process import os,time,random def task(): print('%s is running'%os.getpid()) time.sleep(2) print('%s is done'%os.getpid()) if __name__ == '__main__': p = Process(target=task) p.daemon = True # 必须在p.start()之前,守护进程不能开启子进程 p.start() print('主') ''' 举例说明守护进程的应用场景: 假设有两个任务要干,要玩出并发的效果,使用进程的话可以让主进程执行一个任务 然后开启一个子进程执行一个任务。 如果这两个任务毫无关系,那么就直接开启一个子进程 如果主进程的任务在执行完毕后,子进程的任务没有存在的意义了,那么该子进程应该 在开启之前就被设置成守护进程 ''' # 迷惑人的例子 #主进程代码运行完毕,守护进程就会结束 from multiprocessing import Process from threading import Thread import time def foo(): print(123) time.sleep(1) print("end123") def bar(): print(456) time.sleep(3) print("end456") if __name__ == '__main__': p1 = Process(target=foo) p2 = Process(target=bar) p1.daemon = True p1.start() p2.start() print("main-------") # 打印该行则主进程代码结束,则守护进程p1应该被终止,可能会有p1任务执行的打印信息123,因为主进程打印main----时,p1也执行了,但是随即被终止
守护线程
对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
# 守护线程:等到该进程内所有非守护线程都运行完才死掉 from threading import Thread import os,time,random def task(): print('%s is running'%os.getpid()) time.sleep(2) print('%s is done'%os.getpid()) if __name__ == '__main__': t = Thread(target=task) t.daemon = True # 必须在p.start()之前 t.start() print('主') # 迷惑人的例子 from multiprocessing import Process from threading import Thread import time def foo(): print(123) time.sleep(1) print("end123") def bar(): print(456) time.sleep(3) print("end456") if __name__ == '__main__': t1 = Thread(target=foo) t2 = Thread(target=bar) t1.daemon = True t1.start() t2.start() print("main-------")
互斥锁
# 进程 from multiprocessing import Process,Lock import os,time,random def task(mutex): mutex.acquire() print('%s print 1'%os.getpid()) time.sleep(random.randint(1,3)) print('%s print 2'%os.getpid()) mutex.release() if __name__ == '__main__': mutex = Lock() p1 = Process(target=task,args=(mutex,)) p2 = Process(target=task,args=(mutex,)) p3 = Process(target=task,args=(mutex,)) p1.start() p2.start() p3.start() # 线程 from threading import Thread,Lock import time n = 100 def task(): # global n # mutex.acquire() # temp = n # time.sleep(0.1) # n = temp - 1 # mutex.release() global n with mutex: temp = n time.sleep(0.1) n = temp - 1 if __name__ == '__main__': mutex = Lock() t_l = [] for i in range(100): t = Thread(target=task) t_l.append(t) t.start() for t in t_l: t.join() print(n)
加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。 虽然可以用文件共享数据实现进程间通信,但问题是:
1.效率低(共享数据基于文件,而文件是硬盘上的数据)
2.需要自己加锁处理
因此我们最好找寻一种解决方案能够兼顾:1、效率高(多个进程共享一块内存的数据)2、帮我们处理好锁问题。这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。
1 队列和管道都是将数据存放于内存中
2 队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来
我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性
模拟抢票
# 创建db.txt文件 # {"count": 1} from multiprocessing import Process,Lock import json,os,time,random def search(): with open('db.txt',encoding='utf-8') as f: dic = json.load(f) print('%s 剩余票数 %s'%(os.getpid(),dic['count'])) def get(): with open('db.txt',encoding='utf-8') as read_f: dic = json.load(read_f) if dic['count'] > 0: dic['count'] -= 1 time.sleep(random.randint(1,3)) # 模拟手速和网速 with open('db.txt', 'w', encoding='utf-8') as write_f: json.dump(dic,write_f) print('%s 抢票成功'%os.getpid()) def task(mutex): search() mutex.acquire() get() mutex.release() if __name__ == '__main__': mutex = Lock() for i in range(20): p = Process(target=task,args=(mutex,)) p.start()
信号量
互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去,如果指定信号量为3,那么来一个人获得一把锁,计数加1,
当计数等于3时,后面的人均需要等待。一旦释放,就有人可以获得一把锁,信号量与进程池的概念很像,但是要区分开,信号量涉及到加锁的概念 from multiprocessing import Process,Semaphore import time,random def go_wc(sem,user): sem.acquire() print('%s 占到一个茅坑' %user) time.sleep(random.randint(0,3)) #模拟每个人拉屎速度不一样,0代表有的人蹲下就起来了 sem.release() if __name__ == '__main__': sem=Semaphore(5) p_l=[] for i in range(13): p=Process(target=go_wc,args=(sem,'user%s' %i,)) p.start() p_l.append(p) for i in p_l: i.join() print('============》')
Event事件
线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行
event.isSet():返回event的状态值; event.wait():如果 event.isSet()==False将阻塞线程; event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度; event.clear():恢复event的状态值为False。
进程与线程类似,不过进程需要将event = Event()的对象传入函数
定时器
指定n秒后执行某操作
from threading import Timer def hello(): print("hello, world") t = Timer(1, hello) t.start() # after 1 seconds, "hello, world" will be printed
进程队列和线程队列
from multiprocessing import Queue # 进程队列 import queue # 线程队列 q = Queue(3) q.put({'a':1}) q.put('xxxx') q.put(3) print(q.get()) print(q.get()) print(q.get()) # 队列 q = queue.Queue(3) q.put({'a':1}) q.put('xxxx') q.put(3) print(q.get()) print(q.get()) print(q.get()) # 优先级队列,数字越小,优先级越高 q = queue.PriorityQueue(3) q.put((10,{'a':1})) q.put((-1,'xxxx')) q.put((0,3)) print(q.get()) print(q.get()) print(q.get()) # 堆栈 q = queue.LifoQueue(3) q.put({'a':1}) q.put('xxxx') q.put(3) print(q.get()) print(q.get()) print(q.get())
生产者消费者模型
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
from multiprocessing import Queue,Process import time,random,os def producer(q): for i in range(10): res = '包子%s'%i time.sleep(0.5) q.put(res) print('%s 生产了 %s'%(os.getpid(),res)) def consumer(q): while True: res = q.get() if res is None:break print('%s 吃 %s'%(os.getpid(),res)) time.sleep(random.randint(2,3)) if __name__ == '__main__': q = Queue() p = Process(target=producer,args=(q,)) c = Process(target=consumer,args=(q,)) p.start() c.start() p.join() q.put(None) print('主')
生产者消费者模型高级一点的方法
#JoinableQueue([maxsize]):这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。 #参数介绍: maxsize是队列中允许最大项数,省略则无大小限制。 #方法介绍: JoinableQueue的实例p除了与Queue对象相同的方法之外还具有: q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常 q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
from multiprocessing import Queue,Process,JoinableQueue import time,random,os def producer(food,q): for i in range(3): res = '%s%s'%(food,i) time.sleep(0.5) q.put(res) print('%s 生产了 %s'%(os.getpid(),res)) q.join() def consumer(q): while True: res = q.get() print('%s 吃 %s'%(os.getpid(),res)) time.sleep(random.randint(2,3)) q.task_done() if __name__ == '__main__': q = JoinableQueue() p1 = Process(target=producer,args=('包子',q)) p2 = Process(target=producer,args=('泔水',q)) p3 = Process(target=producer,args=('狗粮',q)) c1 = Process(target=consumer,args=(q,)) c2 = Process(target=consumer,args=(q,)) c1.daemon = True c2.daemon = True p1.start() p2.start() p3.start() c1.start() c2.start() p1.join() p2.join() p3.join() # 生产者结束--->q.join()--->消费者确实把所有数据都收到 print('主',os.getpid())
GIL解释器锁
在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
首先需要明确的一点是GIL
并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念,所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL
GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。
可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。
每一个cpython进程内都有一个GIL
GIL导致同一进程内的多个线程同一时间只能有一个运行
之所以有GIL,是因为cpython的内存管理不是线程安全的
对于计算密集型用多进程,对于IO密集型用多线程
死锁和递归锁
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁
from threading import Thread,Lock,RLock import time # mutexA = Lock() # mutexB = Lock() mutexA = mutexB = RLock() # 一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止 class MyThread(Thread): def run(self): self.f1() self.f2() def f1(self): mutexA.acquire() print('%s 拿到A锁'%self.name) mutexB.acquire() print('%s 拿到A锁' % self.name) mutexB.release() mutexA.release() def f2(self): mutexB.acquire() print('%s 拿到B锁'%self.name) time.sleep(0.1) mutexA.acquire() print('%s 拿到A锁' % self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t = MyThread() t.start()
协程
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的
利用yield实现单线程下的并发
import time def consumer(): '''任务1:接收数据,处理数据''' while True: x=yield def producer(): '''任务2:生产数据''' g = consumer() next(g) for i in range(10000000): g.send(i) start = time.time() # 基于yield保存状态,实现两个任务直接来回切换,即并发的效果 # PS:如果每个任务中都加上打印,那么明显地看到两个任务的打印是你一次我一次,即并发执行的. producer() stop = time.time() print(stop-start) # 2.0272178649902344
gevent模块
time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了
from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前
或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头
# pip3 install gevent # 1.切换+保存状态 # 2.检测IO,实现遇到IO切换 from gevent import monkey monkey.patch_all() # 将程序中的所有IO操作打标记,使gevent能识别 import gevent import time def eat(name): print('%s eat 1'%name) time.sleep(2) print('%s eat 2' % name) def play(name): print('%s play 1'%name) time.sleep(3) print('%s play 2' % name) g1 = gevent.spawn(eat,'alex') g2 = gevent.spawn(play,'egon') g1.join() g2.join()
IO阻塞模型(blocking IO)
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
所以,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。
from socket import * server = socket(AF_INET,SOCK_STREAM) server.bind(('127.0.0.1',8080)) server.listen(5) while True: conn,addr = server.accept() print(addr) while True: try: data = conn.recv(1024) if not data:break conn.send(data.upper()) except ConnectionResetError: break conn.close()
from socket import * client = socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: msg = input('>>:').strip() if not msg:continue client.send(msg.encode('utf-8')) data = client.recv(1024) print(data.decode('utf-8')) client.close()
非阻塞IO模型
Linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
# 1.对cpu的占用率过多,但是是无用的占用 # 2.在链接数过多的情况下不能及时响应客户端的消息 from socket import * server = socket(AF_INET,SOCK_STREAM) server.bind(('127.0.0.1',8080)) server.listen(5) server.setblocking(False) # 非阻塞型,默认为阻塞型True conn_l = [] while True: try: conn,addr = server.accept() conn_l.append(conn) print(addr) except BlockingIOError: # print('干其它活去了') # time.sleep(2) del_l = [] for conn in conn_l: try: data = conn.recv(1024) if not data: # 针对linux系统 conn.close() del_l.append(conn) continue conn.send(data.upper()) except BlockingIOError: pass except ConnectionResetError: conn.close() del_l.append(conn) for conn in del_l: conn_l.remove(conn)
from socket import * client = socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1',8081)) while True: msg = input('>>:').strip() if not msg:continue client.send(msg.encode('utf-8')) data = client.recv(1024) print(data.decode('utf-8')) client.close()
IO多路复用
IO multiplexing这个词可能有点陌生,但是如果我说select/epoll,大概就都能明白了。有些地方也称这种IO方式为事件驱动IO(event driven IO)。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
强调:
1. 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
2. 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
结论: select的优势在于可以处理多个连接,不适用于单个连接
from socket import * import select server = socket(AF_INET,SOCK_STREAM) server.bind(('127.0.0.1',8080)) server.listen(5) server.setblocking(False) # 非阻塞型,默认为阻塞型True read_l = [server,] print('strating....') while True: rl,wl,xl = select.select(read_l,[],[]) # 整体的返回值是一个元组,rl为元组里的一个列表 # print('===>',rl) # rl里的值就是server对象或conn对象 for r in rl: if r is server: conn,addr = r.accept() read_l.append(conn) else: try: data = r.recv(1024) if not data: r.close() read_l.remove(r) r.send(data.upper()) except ConnectionResetError: r.close() read_l.remove(r)
from socket import * client = socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1',8081)) while True: msg = input('>>:').strip() if not msg:continue client.send(msg.encode('utf-8')) data = client.recv(1024) print(data.decode('utf-8')) client.close()
socketserver模块
TCP
import socketserver class MyTCPHandler(socketserver.BaseRequestHandler): def handle(self): print('========?>',self.request) # self.request is conn while True: data = self.request.recv(1024) self.request.send(data.upper()) if __name__ == '__main__': # socketserver.ForkingTCPServer 这个模块的多进程只能在linux上用 server = socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyTCPHandler) server.serve_forever()
from socket import * client = socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1',8081)) while True: msg = input('>>:').strip() if not msg:continue client.send(msg.encode('utf-8')) data = client.recv(1024) print(data.decode('utf-8')) client.close()
UDP
import socketserver class MyTCPHandler(socketserver.BaseRequestHandler): def handle(self): print('========?>',self.request) # self.request 是一个元组,第一个值是客户端发来的消息,第二个值是一个套接字对象 client_data=self.request[0] self.request[1].sendto(client_data.upper(),self.client_address) if __name__ == '__main__': # socketserver.ForkingTCPServer 这个模块的多进程只能在linux上用 server = socketserver.ThreadingUDPServer(('127.0.0.1',8080),MyTCPHandler) server.serve_forever()
from socket import * client = socket(AF_INET,SOCK_DGRAM) while True: msg = input('>>:').strip() if not msg:continue client.sendto(msg.encode('utf-8'),('127.0.0.1',8080)) data,server_addr = client.recvfrom(1024) print(data.decode('utf-8')) client.close()
paramiko模块
paramiko是一个用于做远程控制的模块,使用该模块可以对远程服务器进行命令或文件操作,值得一说的是,fabric和ansible内部的远程管理就是使用的paramiko来现实
下载安装
pip3 install paramiko #在python3中
SSHClient
用于连接远程服务器并执行基本命令
基于用户名密码连接:
import paramiko # 创建SSH对象 ssh = paramiko.SSHClient() # 允许连接不在know_hosts文件中的主机 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # 连接服务器 ssh.connect(hostname='120.92.84.249', port=22, username='root', password='xxx') # 执行命令 stdin, stdout, stderr = ssh.exec_command('df') # 获取命令结果 result = stdout.read() print(result.decode('utf-8')) # 关闭连接 ssh.close()
基于公钥密钥连接:
客户端文件名:id_rsa
服务端必须有文件名:authorized_keys(在用ssh-keygen时,必须制作一个authorized_keys,可以用ssh-copy-id来制作)
import paramiko private_key = paramiko.RSAKey.from_private_key_file('/tmp/id_rsa') # 创建SSH对象 ssh = paramiko.SSHClient() # 允许连接不在know_hosts文件中的主机 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # 连接服务器 ssh.connect(hostname='120.92.84.249', port=22, username='root', pkey=private_key) # 执行命令 stdin, stdout, stderr = ssh.exec_command('df') # 获取命令结果 result = stdout.read() print(result.decode('utf-8')) # 关闭连接 ssh.close()
SFTPClient
用于连接远程服务器并执行上传下载
基于用户名密码上传下载
import paramiko transport = paramiko.Transport(('120.92.84.249',22)) transport.connect(username='root',password='xxx') sftp = paramiko.SFTPClient.from_transport(transport) # 将location.py 上传至服务器 /tmp/test.py sftp.put('/tmp/id_rsa', '/etc/test.rsa') # 将remove_path 下载到本地 local_path sftp.get('remove_path', 'local_path') transport.close()