参考资料:这里
本节内容:
-
什么是线程
-
什么是进程
-
线程与进程的区别
-
守护线程
-
为什么会有GIL锁
-
递归锁
-
信号量
-
event
进程:QQ要以一个整体的形式暴露给操作系统管理,里面包含对各种资源的调用,内存的管理,网络接口的调用等...对各种资源管理的集合,就可以成为进程。相当于房间
线程:是操作系统的最小的调度单位,是一串指令的集合。相当于我们每一个人。
进程要操作cpu,必须先创建一个线程。相当于一间房子,必须要有人才显示其价值,不然就是一个空盒子
什么是线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位,一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程就是cpu执行时所需要的一段执行的上下文。
假设你读一本书,现在需要休息,等你休息完回来之后继续从上一次停顿的地方读下去。需要记下读到的地方。
现在有一个朋友读同一本书,每次停顿的时候也要记下停顿的地方 ,然后将书本还给你,继续读下去。
线程按照同样的方法工作的。
一个CPU只不过给了你一个幻觉,以为他是同时进行多个运算(单核只能做一件事,双核同时做两件事),因为CPU运算速度太快了,每秒运算超过上亿次。实际上一会运算下QQ,一会运算下Word。
很多的任务共享一个CPU!!
线程与进程不一样,线程是一段上下文的指令,进程是运算时一堆相关资源的集合,一个进程可以包含一个线程,也可以包含多个线程。
一个进程占据内存一段空间,这个进程内所有线程共享这段内存空间。
相当于一间房子占据世界一段空间,房子里的每个人都可以走到这间房子的位置。
什么是进程
每一个进程提供程序需要的资源:一个进程需要内存里的虚拟地址空间,可执行代码调用了操作系统的接口,安全的上下文,唯一的进程标识符,一个优先级类,最小、最大的工作内存空间,和至少一个线程。
每个进程开始于一个线程,叫主线程,也可以创建额外的线程
进程与线程的区别
进程快还是线程快?
根本没有可比性!!
启动一个进程快还是一个线程快?
启动一个线程快!因为启动一个线程,相当于拉一个人,启动一个进程,相当于建房子
区别在于:
1.线程共享内存空间,进程的内存是独立的
2.线程可以访问所在进程的所有资源,多个子进程只有他们复制的自己的父进程的数据(每一次创建子进程,相当于对父进程的克隆,就如将父房子,复制一个出来)
3.同一个进程的线程之间可以直接交流数据(数据的共享,信息的传递);两个进程想通信,必须要经过第三方(中间代理)来实现
4.创建新线程很简单,创建新进程需要对其父进程进行一次克隆
5.一个线程可以控制和操作同一进程里的其他线程;但是进程只能操作子进程
6.对于主线程的修改,会影响同一个进程其他线程的行为;但是对于一个父进程的修改,不会影响子进程。(只要不删除父进程就不会有影响)
线程代码区
import threading import time def run(n): print("talk ",n) time.sleep(2) t1 = threading.Thread(target=run,args = ("t1",)) t1.start() t2 = threading.Thread(target=run,args = ("t2",)) t2.start() print("end....")
import threading import time class Mythreading(threading.Thread): def __init__(self,n): super(Mythreading,self).__init__() self.n = n def run(self): print("task ",self.n) t1 = Mythreading("t1") t2 = Mythreading("t2") t1.start() t2.start()
# 主线程内容,主线程与其他线程的并行的,不是串行
# 默认情况下,主线程是不子线程的结果的
import threading import time def run(n): print("task ",n) time.sleep(2) # 主线程内容 start_time =time.time() # 主线程内容 for i in range(50): # 其他线程 t = threading.Thread(target=run,args=("t%s" %i ,)) t.start() # 主线程内容 print("cost:",time.time()-start_time) print("end....") # 主线程内容,主线程与其他线程的并行的,不是串行 # 默认情况下,主线程是不子线程的结果的
''' 这里将sleep的时间也传递进来,若线程t1停顿2秒,线程t2停顿4秒,等待线程t1运行后才运行主线程,不停顿线程t2 结果: talk t1 talk t2 t1 thread end main thread end.... t2 thread end ''' import threading import time def run(n,sleep_time): print("talk ",n) time.sleep(sleep_time) print(" %s thread end"%n) t1 = threading.Thread(target=run,args = ("t1",2)) t2 = threading.Thread(target=run,args = ("t2",4)) # 线程的开始 t1.start() t2.start() # 等待t1运行结果,使得线程变成了串行 t1.join() # t2.join() print("main thread end....")
50个线程,需要实现并行,而且线程完了之后,主线程才继续走下去,所以需要等待所有线程完毕后才能继续下一步
这样就能计算到整个程序花费的时间
''' 50个线程,需要实现并行,而且线程完了之后,主线程才继续走下去,所以需要等待所有线程完毕后才能继续下一步 这样就能计算到整个程序花费的时间 ''' import threading import time def run(n): print("task ",n) time.sleep(2) print("task %s done..."%n) # 主线程内容 start_time =time.time() thread_obj_list = [] # 主线程内容 for i in range(50): # 其他线程 t = threading.Thread(target=run,args=("t%s" %i ,)) t.start() thread_obj_list.append(t) for i in thread_obj_list: i.join() # print("%s thread end"%i) # 主线程内容 print("cost:",time.time()-start_time) print("main thread end....") # 主线程内容,主线程与其他线程的并行的,不是串行 # 默认情况下,主线程是不子线程的结果的
守护线程
守护线程:服务于非守护线程,当非守护线程结束后,守护线程也结束了。
threading.current_thread()#显示当前显示是什么,还有对应的pid,如果是子线程,显示:task 【<Thread(Thread-50, started daemon 7496)>】 t49,如果是主线程会显示:<_MainThread(MainThread, started 5392)>
threading.active_count()#显示当前执行线程的总数
守护线程:
一个进程可以启动很多个守护线程,但是当进程没有了,守护线程也结束了。进程相当于皇帝,守护线程是大臣,皇帝死了,需要他们做殉葬。
守护线程可以做很多事情:。。。。。
当其他线程变成守护线程的时候,主线程就不会再等待线程结束后继续(哪怕有线程.join()等待也一样,一般变成守护线程后,就不需要join了)
线程.setDaemon(True)#把当前线程设置为守护线程
线程.start()#设置守护线程必须在start之前
实际应用场景:socketserver,手动停服务的时候,就不能等线程结束而直接结束;
import threading import time def run(n): print("task 【%s】"%threading.current_thread(),n) time.sleep(2) # 主线程内容 start_time =time.time() # 主线程内容 for i in range(50): # 其他线程 t = threading.Thread(target=run,args=("t%s" %i ,)) t.setDaemon(True)#把当前线程设置为守护线程,设置守护线程必须在start之前 t.start() # 主线程内容 print("cost:",time.time()-start_time) print("main thread:【%s】 end....,总线程数为【%s】"%(threading.current_thread(),threading.active_count())) # 主线程内容,主线程与其他线程的并行的,不是串行 # 默认情况下,主线程是不子线程的结果的
为什么会有GIL锁(全局解析器锁)
我有四核的时候,就能真真正正的干四个任务;但是python(python的线程是调用操作系统的原生线程,也就是C-python,现有的版本都是这个)中,无论你有多少核,同一时间内只有一个线程执行,这是python开发设计时候的缺陷。
如果四个线程同时修改同一个数,因为执行之后,都是同时执行,所以每个线程都修改这个数,导致得出结果不正确,所以python自己给自己加了一个锁,只允许同一时间只有一个线程拿到这个数据,只有一个线程可以修改。
递归锁(实际比较少用)
假设进入大门需要刷卡,进入课室也要刷卡,你忘记带走某些东西,需要回学校拿。学校有个变态的规定,某个时间段只能一个人进入,学校里只有你进来。进入大门后,将大门关闭,然后进入第二道门,之后第二道门也锁了。
出去的时候,先出第二道门,需要解锁,出大门也要解锁。
但是人太笨了(哈哈哈),分不出哪把钥匙是对应门的,所以无法出来。
import threading, time def run1(): print("grab the first part data") lock.acquire() global num num += 1 lock.release() return num def run2(): print("grab the second part data") lock.acquire() global num2 num2 += 1 lock.release() return num2 def run3(): lock.acquire() res = run1() print('--------between run1 and run2-----') res2 = run2() lock.release() print(res, res2) if __name__ == '__main__': num, num2 = 0, 0 lock = threading.RLock() for i in range(1): t = threading.Thread(target=run3) t.start() while threading.active_count() != 1: print(threading.active_count()) else: print('----all threads done---') print(num, num2)
信号量
互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据,比如厕所有三个坑,那最多只允许3个人上厕所,后面的人只能等里面的人出来了才能再进去。
使用的过程中是出来一个,就能进入下一个,而不是三个都同时出来了,才能继续进去。
import threading import time def run(n): # 信号量获取跟锁一样,先获取锁,然后释放,信号量可以有多把锁, semaphore.acquire() time.sleep(1) print("运行线程:%s"%n) # 信号量的释放 semaphore.release() # 最多允许5个线程同时运行 semaphore = threading.BoundedSemaphore(5) # 生成20个线程,运行方式是run,传递的值为元祖(i,) for i in range(22): t = threading.Thread(target=run,args=(i,)) t.start() while threading.active_count() !=1: pass else: print("所有线程已经完毕!")
event(事件)
每一次状态的变换就是事件的变化,具体例子就是红绿灯
一个事件就是简单的同步对象。
event = threading.Event()#生成一个事件对象
event.set()#设置一个全局变量标志位,这里将标志位改为True
event.clear()#讲全局变量标志位清空,这里将标志位改为False
event.wait()#等着标志位被设置
如果标志位被设定,等待的方法不会做任何事情,代表绿灯,直接通行;
如果标志位被清空,等待阻塞直至标志位被设定;代表红灯,wait等待变绿灯;
所有的现场都可以等待同样的事件;
import time import threading event = threading.Event() def lighter(): count = 0 event.set()#设置绿灯 while True: if count > 5 and count <10: # 改成红灯 event.clear()#把标志位清空 print("\033[41;1m现在是红灯\033[0m".center(70,"="),count) elif count > 10: event.set()#变绿灯 count = 0#重新计数 else: print("\033[42;1m现在是绿灯\033[0m".center(70,"="),count) time.sleep(1) count +=1 def car(name): ''' 车要不停的检测红绿灯 :param name: :return: ''' while True: # 如果设置了标志位,代表绿灯 if event.is_set(): print("【%s】 车跑!"%name) time.sleep(1) else: print("【%s】红灯,车在等待"%name) event.wait() print("\033[34;1m【%s】 绿灯,开始走!\033[0m"%name) light = threading.Thread(target=lighter,) light.start() car1 = threading.Thread(target=car("特斯拉",)) car1.start()
队列queue
优点:
1.提高效率;例子:假设你要提供什么资料给我,资料放在硬盘,派对给资料的人有100人,如果每个人排队给我,这样就浪费了排队人的效率;但是如果他们将硬盘放在桌子上排队的话,他们就能做自己的事情了。
这样做的话,我看着他们没有这么多人排队,压力也少了,所以效率也提高了。
也可以我这边增加一个人收集资料,这样排成两条队列,更加提高效率。
2.完成了程序的解耦(主要作用);硬盘放在桌子上,我跟给资料的人就没关系了,只与桌子有关系
他们与我也没啥关系了。
队列与列表的区别
队列就是一个有顺序的容器。
列表也是有顺序的容器;
他们的区别是:列表取出数据后,数据还在列表中,队列取出数据后,数据则不在队列了(数据只有一份,取走就没了)。
队列分为三种,如下
queue.Queue(maxsize=0)#先入先出,若maxsize设定了值,代表队列有最大存储限制,超过这限制继续存放数据会导致卡住。
queue.LifoQueue(maxsize=0)#后进先出(卖水果的例子,新鲜的容易被买)
queue.PriorityQueue(maxsize=0)#存储数据时可设置优先级的队列;例如:VIP的例子
方法:
Queue.put(item,block=True,timeout=None)#往队列放数据
Queue.qsize()#判断队列大小
Queue.full()#当队列满的时候返回True;设置了队列的大小后,就能用到
Queue.empty()#如果队列为空,返回True
Queue.get(block=True,timeout=None)#block是阻塞,取不到数据就会卡住;timeout超时时间默认为无,可以设置超时时间。例如设置timeout=1
Queue.get_nowait()#获取队列数据,有数据的直接读取数据,没有数据的跑出queue。Empty()异常
Queue.done()#
import queue # 生成一个队列对象.这里是先进先出 queue = queue.Queue() queue.put("d1") queue.put("d2") queue.put("d1") # 打印队列的大小 print(queue.qsize()) # 获取第一个数据 print(queue.get()) # 获取第二个数据 print(queue.get()) # 获取第三个数据 print(queue.get()) # 若队列没有了数据,再读取,则导致卡着 # print(queue.get())
import queue q = queue.LifoQueue() q.put(1) q.put(2) q.put(3) print(q.get()) print(q.get()) print(q.get()) ''' 结果: 3 2 1 '''
import queue q = queue.PriorityQueue() # 用元祖写上排序,元祖第一个元素就是序号 q.put((4,1)) q.put((5,2)) q.put((1,4)) print(q.get()) print(q.get()) print(q.get())
import queue import threading import time q = queue.Queue(5) def Producer(name): count = 0 while True: count +=1 q.put("骨头") print("生产了一个骨头:%s,总数:%s"%(count,q.qsize())) time.sleep(1) def Consumer(name): while True: print("【%s】取到【%s】并且吃掉它"%(name,q.get())) time.sleep(3) p = threading.Thread(target=Producer,args=("Alex",)) c = threading.Thread(target=Consumer,args=("Dog",)) d = threading.Thread(target=Consumer,args=("Cat",)) p.start() c.start() d.start()
多进程
import multiprocessing
基本语法与线程差不多
生成进程:multiprocessing.Process(target=run,)
每一个进程都有其父进程
注意:必须要有if __name__ == '__main__':
import multiprocessing def run(n): print("多进程方法,传递的n:",n) if __name__ == '__main__':#进程必须要有这句 m = multiprocessing.Process(target=run,args=("Alex",)) m.start()
import multiprocessing import time def run(name): time.sleep(1) print("hello!",name) if __name__ == '__main__': for i in range(10): m = multiprocessing.Process(target=run,args=("%s"%i,)) m.start() m.join()
import multiprocessing import os def info(info): print(info) print("module name:",__name__) print("父进程id:",os.getppid()) print("process id:",os.getpid()) print("\n\n") def f(name): info("\033[42;1m function f\033[0m") print(name) if __name__ == '__main__': info("\033[41;1m main function\033[0m") m = multiprocessing.Process(target=f,args=("Alex",)) m.start() ''' C:\Python36\python.exe E:/cheng/study/Python/code_245/fourth/learn/线程与进程/多进程/get_进程id.py main function module name: __main__ 父进程id: 4680 process id: 5292 function f module name: __mp_main__ 父进程id: 5292 process id: 8948 Alex 进程已结束,退出代码0 '''
进程间数据交互
一般情况下,进程间的数据是不交互的。
进程间数据交互
不同进程间内存是不共享的,要想实现两个进程的数据交互,可以用以下三种方法:
Queues(子进程不能访问父进程的数据,两者是独立的内存),所以这里就引入了multiprocessing.Queue()、Manager()等来处理。
下方代码的实际是克隆了一个q,交给了子进程,子进程网q里面放了数据。
再将数据pickle给另一个中间介,然后弄到主进程的队列。
这样的通信相当于实现了数据的传递(queue,pipe)
1.队列:Queue
# 子进程传入队列,数据,主进程访问队列数据 import multiprocessing def f(q): q.put([42,None,"hello"]) if __name__ == '__main__': q = multiprocessing.Queue() # 生成一个子进程,调用函数f,传递参数为q p = multiprocessing.Process(target=f,args=(q,)) p.start() # 主进程读取子进程写入的队列的数据 print(q.get())
2.管道:pipe
类似电话线两端连接,也可以说是管道的两端
import multiprocessing def f(conn): # 子进程发送信息给父进程 conn.send([42,None,"hello from child"]) # 子进程接收父进程发送的信息 print("from parent_conn:",conn.recv()) conn.close() if __name__ == '__main__': # 管道生成两个对象,相当于管道的两端,这里一端定义为父亲,一端定义为孩子 parent_conn,child_conn = multiprocessing.Pipe() p = multiprocessing.Process(target=f,args=(child_conn,)) p.start() # 父进程接收子进程发送的信息 print(parent_conn.recv()) # 父进程发送信息给子进程 parent_conn.send("孩子,可好?")
3.Manager():实现数据的共享
import multiprocessing,os def f(d,l): # d[1] = "1" # d["2"] = 2 # d[0.25] = None # os.getpid()每个进程的id d[os.getpid()] = os.getpid() l.append(os.getpid()) print(l) if __name__ == '__main__': with multiprocessing.Manager() as manager:#这里可以写成manager = multiprocessing.Manager() #生成一个字典,可以在多个进程间共享和传递 d = manager.dict() # {} # 生成一个列表,可以在多个进程间共享和传递 l = manager.list(range(5)) # 先写入0-4五个数据 p_list = [] for i in range(10): p = multiprocessing.Process(target=f, args=(d, l)) p.start() p_list.append(p) # 因为每个进程都修改共享信息,所以用join等待结果执行完毕,能够区分 for res in p_list: res.join() print(d) print(l)
进程也是有锁的:
这个锁是用于防止进程抢占显示屏幕等资源而存在,不会让同一时间不同进程抢着打印自己的信息。
import multiprocessing def f(l,i): # 获取信息 l.acquire() print("hello world:",i) # 释放信息 l.release() if __name__ == '__main__': # 生成了锁对象 lock = multiprocessing.Lock() # 生成十个进程,调用f方法生成子进程,传递lock对象以及对应数字 for num in range(10): p = multiprocessing.Process(target=f,args=(lock,num)) p.start()
进程池
生成一个进程就相当于复制一份父进程的数据,假设我复制100份,每份一个G,占用的资源就会非常多,所以为了类似的问题导致系统崩溃,就引入了进程池的信息。
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。
pool = multiprocessing.Pool(processes=None, initializer=None, initargs=(),maxtasksperchild=None, context=None)
pool = multiprocessing.Pool(5),相当于pool = multiprocessing.Pool(processes=5)
进程池有两个方法:
1.apply(func=Foo , args = (a,b)) 串行
from multiprocessing import Process,Pool import time,os def Foo(i): time.sleep(1) print("in process:",os.getpid()) return i + 100 def Bar(arg): print("--->exec done",arg) # 为了区分主动调用这段代码还是其他地方导入,如果是导入的情况,这里不执行 if __name__ == '__main__': pool = Pool(5) for i in range(10): # 并行,callback是回调,意思是执行完Foo后,再执行Bar # pool.apply_async(func=Foo,args=(i,),callback=Bar) # 串行 pool.apply(func=Foo,args=(i,)) print("end") pool.close() # 进程池中进程执行完毕后再关闭,如果注释,则程序直接关闭;这个不要放到close之前,不然会出现各种奇怪的问题 pool.join()
2.apply_async(func =Foo ,args = (a,b),callback =Bar ) 并行,callback是很有用的,意思是执行完Foo后,再执行Bar,这个callback是主进程调用的
from multiprocessing import Process,Pool import time,os def Foo(i): time.sleep(1) print("in process:",os.getpid()) return i + 100 def Bar(arg): print("--->exec done",arg) # 为了区分主动调用这段代码还是其他地方导入,如果是导入的情况,这里不执行 if __name__ == '__main__': pool = Pool(5) for i in range(10): # 并行,callback是回调,意思是执行完Foo后,再执行Bar pool.apply_async(func=Foo,args=(i,),callback=Bar) # 串行 # pool.apply(func=Foo,args=(i,)) print("end") pool.close() # 进程池中进程执行完毕后再关闭,如果注释,则程序直接关闭;这个不要放到close之前,不然会出现各种奇怪的问题 pool.join()
callback使用场景:发送十个命令让数据库备份的时候,备份完毕,调用写入日志,就不需要每个子进程都再次连接数据库,将会增加效率
from multiprocessing import Process,Pool import time,os def Foo(i): time.sleep(1) print("in process:",os.getpid()) return i + 100 def Bar(arg): print("--->exec done",arg,os.getpid()) # 为了区分主动调用这段代码还是其他地方导入,如果是导入的情况,这里不执行 if __name__ == '__main__': pool = Pool(5) print("主进程pid:",os.getpid()) for i in range(10): # 并行,callback是回调,意思是执行完Foo后,再执行Bar pool.apply_async(func=Foo,args=(i,),callback=Bar) # 串行 # pool.apply(func=Foo,args=(i,)) print("end") pool.close() # 进程池中进程执行完毕后再关闭,如果注释,则程序直接关闭;这个不要放到close之前,不然会出现各种奇怪的问题 pool.join()
协程
CPU只认识线程,CPU不知道协程的存在。
协程拥有自己的寄存器上下文何栈。将寄存器上下文何栈保存到其他地方,在切回来的时候,回复先前保存的寄存器上下文和栈。因此;协程能够保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法装开时所处逻辑流的位置。
类似yelid的例子
协程的好处:
1.无需线程上下文切换的开销;(单线程下运行,不是多线程)
2.无需原子操作(更改变量等的操作)锁定及同步的开销;(同样因为单线,不涉及多线程,所以不需要锁等操作)
3.方便切换控制流,简化编程模型;
4.高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
1.无法利用多核资源:协程的本质是个单线程,她不能同时将 单个CPU的多个核用上,协程需要和进程配合才能运行在多CPU上,当然我们日常所编写的绝大部分应用都没有这个必要,除非是CPU密集型应用。
2.进行阻塞(Blocking)操作(如IO时)会阻塞整个程序。
协程的例子
def producer(): # 因为consumer第一次执行,变成了生成器,还没真正执行,所以需要__next__一下才执行 r1 = c1.__next__() r2 = c2.__next__() n =0 while n<5: n+=1 # 唤醒生成器的同时,传递一个值,这里是n c1.send(n) c2.send(n) print("\033[42;1m 生产者制造包子: \033[0m".center(70,"="),n) def consumer(name): print("开始吃包子".center(70,"=")) while True: new_baozi = yield print("【%s】在吃包子【%s】" %(name,new_baozi)) if __name__ == '__main__': c1 = consumer("c1") c2 = consumer("c2") p = producer()
单线程下做到类似并发的效果,就是遇到IO就切换
所以就需要引入Greenlet
安装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() ''' 结果: 12 56 34 78 '''
from urllib import request def f(url): print("get:%s" %url) # 打开一个URL resp = request.urlopen(url) # 读取里面的信息 data = resp.read() # 保存到一个文件 f = open("url.html","wb") f.write(data) f.close() print("%d bytes received from %s."%(len(data),url)) f("http://www.cnblogs.com/cheng662540/p/8481261.html")
from urllib import request import gevent,time def f(url): print("get:%s" %url) # 打开一个URL resp = request.urlopen(url) # 读取里面的信息 data = resp.read() print("%d bytes received from %s."%(len(data),url)) time_start = time.time() urls = [ "https://www.python.org/", "https://www.yahoo.com/", "https://github.com/" ] for url in urls: f(url) print("同步花费总时间为:",time.time() - time_start) ''' 结果: get:https://www.python.org/ 48872 bytes received from https://www.python.org/. get:https://www.yahoo.com/ 528128 bytes received from https://www.yahoo.com/. get:https://github.com/ 52366 bytes received from https://github.com/. 同步花费总时间为: 9.30053162574768 '''
''' 自动IO切换 ''' # urllib通过gevent调用,检测不到IO操作,所以不会切换,变成了串行 from urllib import request import gevent,time def f(url): print("get:%s" %url) # 打开一个URL resp = request.urlopen(url) # 读取里面的信息 data = resp.read() print("%d bytes received from %s."%(len(data),url)) start_time = time.time() gevent.joinall([ gevent.spawn(f,"https://www.python.org/"), gevent.spawn(f,"https://www.yahoo.com/"), gevent.spawn(f,"https://github.com/") ]) print("异步花费总数据为:",time.time() - start_time) ''' 结果: get:https://www.python.org/ 48872 bytes received from https://www.python.org/. get:https://www.yahoo.com/ 533641 bytes received from https://www.yahoo.com/. get:https://github.com/ 52366 bytes received from https://github.com/. 异步花费总数据为: 7.8384482860565186 '''
加上这两段代码,把当前程序的所有的IO操作给我单独的坐上标记,然后urllib需要阻塞的地方都阻塞了,做到真正的异步
from gevent import monkey
monkey.patch_all()
''' 自动IO切换 ''' # urllib通过gevent调用,检测不到IO操作,所以不会切换,变成了串行 from urllib import request from gevent import monkey import gevent,time # 把当前程序的所有的IO操作给我单独的坐上标记,然后urllib需要阻塞的地方都阻塞了,做到真正的异步 monkey.patch_all() def f(url): print("get:%s" %url) # 打开一个URL resp = request.urlopen(url) # 读取里面的信息 data = resp.read() print("%d bytes received from %s."%(len(data),url)) start_time = time.time() gevent.joinall([ gevent.spawn(f,"https://www.python.org/"), gevent.spawn(f,"https://www.yahoo.com/"), # gevent.spawn(f,"https://github.com/") ]) print("异步花费总数据为:",time.time() - start_time) ''' 结果: get:https://www.python.org/ get:https://www.yahoo.com/ 48872 bytes received from https://www.python.org/. 532214 bytes received from https://www.yahoo.com/. 异步花费总数据为: 1.418081283569336 '''
通过gevent实现单线程下的多socket并发
import sys import socket import gevent import time from gevent import socket,monkey monkey.patch_all() def server(port): # 生成socket对象 s = socket.socket() # 绑定ip地址以及端口 s.bind(('0.0.0.0',port)) # 监听 s.listen(500) while True: client,addr = s.accept() # 启动一个协程,将新生成的客户端连接实例交给handle_request方法 gevent.spawn(handle_request,client) def handle_request(conn): try: while True: data = conn.recv(1024) print("接收的数据:",data) # 将回复返回给客户端 conn.send(data) # 如果没有数据,发送一个shutdown标志将客户端断开 if not data: conn.shutdown(socket.SHUT_WR) except Exception as e : print(e) # 最后关闭连接 finally: conn.close() if __name__ == '__main__': server(9999)
import socket HOST = "localhost" PORT = 9999 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect((HOST,PORT)) while True: msg = bytes(input(">>:"),encoding="utf-8") s.sendall(msg) data = s.recv(1024) print("Received",repr(data)) s.close()
论事件驱动与异步IO
通常,我们写服务器处理模型的程序时,有以下几种模型:
(1)每收到一个请求,创建一个新的进程,来处理该请求;
(2)每收到一个请求,创建一个新的线程,来处理该请求;
(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞IO方式来处理请求;(协程)
以上的几种方式,各有千秋
(1)由于创建新的进程开销比较大,所以会导致服务器性能比较查,但是实现比较简单;
(2)由于要涉及到线程的同步,有可能会面临死锁等问题;
(3)在写应用程序代码时,逻辑比前面两种都复杂。
综合考虑各方面因素,一般普遍认为第三种方式是大多数网络服务器采用的方式。
看图说话讲事件驱动模型
在UI编程中,常常要对鼠标点击进行相应,首先如何获取鼠标点击呢?
方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,这种方式有几个缺点:
1,CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多CPU资源的浪费,如果扫描鼠标点击的接口是阻塞的呢?
2.如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,那么可能永远不会去扫描键盘;
3.乳沟一个循环需要扫描的设备非常多,这优惠引来相应时间的问题;
所以,该方式不好
方式二:就是事件驱动模型:根据一个事件,做出反应
目前大部分的UI编程都是事件驱动模型,入很多UI平台都提供onClick()事件,这个事件就代表了鼠标按下事件。事件驱动模型答题思路如下:
1.有一个事件(消息)队列;
2.鼠标按下时,往这个队列中增加一个点击事件(消息)
3.有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等
4.事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。她的特定是包含一个事件循环,当外部事件发生时使用回调除法相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。
处理完一件事件之后,回调执行一个函数,告知执行情况
同步IO与异步IO
linux下的network IO
主要以下五种
-用户空间和内核空间
-进程切换
-进程的阻塞
-文件描述符
-缓存I/O
用户空间和内核空间
现在操作系统都是采用虚拟存储器,对于32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟存储空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFF)供内核使用,成为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,成为用户空间。
进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另一个进程上运行,这个过程中经过以下变化:
1.保存处理器上下文,包含程序计数器何其他寄存器;
2.更新PCB信息;
3.把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列;
4.选择另一个进程执行,并更新起PCB;
5.更新内存管理的数据结果;
6.恢复处理机上下文
总而言之,就是很耗资源。
进程的阻塞
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无心工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获取CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
文件描述符fd:
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象画概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当进程打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些设计底层的程序编写往往会 围绕着文件描述符展开,但是文件描述符这一概念往往只适用于Unix、linux这样的操作系统。
缓存I/O
缓存I/O又被称作为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓存区拷贝到应用程序的地址空间。
类似socket,也有一个缓存区,这个就是系统内核的缓存区,然后才存储到用户的存储空间。
(调用网卡等硬件的时候,用户空间是没有权限访问内核空间的,所以才需要与内核空间交互数据,也就出现了缓存区)
缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。
IO模式
刚才说了,对于一次IO访问(以read举例子),数据会先被拷贝到操作系统内核的缓存区中,然后才会从操作系统内核的缓存区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,会经理两个阶段:
1.等待数据准备;
2.将数据从内核拷贝到进程中;
正是因为这两个阶段,linux系统产生了下面五种网络模式的方案。
-阻塞I/O (blocking IO)
-非阻塞I/O (nonblocking IO)
-I/O多路复用 (Io multiplexing)最常用
-信号驱动I/O (signal driven IO )
-异步I/O (asynchronous IO)
阻塞I/O (blocking IO)
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
特点:blocking IO的特点就是在IO执行的两个阶段都被block了。
非阻塞I/O (nonblocking IO)
(能实现多并发了,但是还会卡着)
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作的时候,流程是这样子
当用户进程发出read操作时,如果内核中的数据还没有准备好,那么它不会阻塞用户进程,而是立刻返回一个error。从该用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一单内核中的数据准备好了,并且又再次受到了哟公户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以nonblocking IO的特点就是用户进程需要不断的主动询问内核数据好了没有。
I/O多路复用 (Io multiplexing)
I/O多路复用就是我们说的select,poll,epoll,有些地方也称这种IO方式为事件驱动IO。select/epoll的好处就在于单个进程就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个方法会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block,同时,内核会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回,这个时候,用户进程再调用read操作,将数据从内核拷贝到用户进程。
所以I/O多路复用的特点就是通过一个机制一个进程能同时等待多个文件描述符,这些文件描述符其中的任意一个进入读就绪状态,select()函数就可以返回。
异步I/O (asynchronous IO)
用户进程发起read操作之后,立刻就可以开始去做其他的事。另一方面,从国内和的角度,当它收到一个asynchronous read之后,首先它 会立刻返回,所以不会对用户进程产生任何block。然后,内核会等待数据准备晚餐,然后将数据拷贝到用户内存,这一切都完成之后,内核会给用户进程发送一个(信号)signal,告诉她read操作完成了。
区别:
阻塞和非阻塞区别:调用阻塞IO会一直阻塞住直到操作完成,而非阻塞IO在内核还准备数据的情况下回立刻返回。
同步IO与异步IO的区别:前三种【阻塞I/O (blocking IO)非阻塞I/O (nonblocking IO)I/O多路复用 (Io multiplexing)】都是同步IO,运行的时候会有阻塞,而异步IO发起操作之后,就不再理会,直到内核发送一个信号,告诉进程说IO完成了。(前者会卡,后者不会卡)
select、poll、epoll
参考:点击这里
select目前几乎所有平台上都支持,缺点是在linux上一般为1024(但是这个可以修改)
poll与select没有本质区别,但是poll没有最大文件描述符数量的限制
epoll(IO复用的)具有之前所说的优点(windows不支持)
有快递了,打电话告诉你了,可是你没取,所以快递一直放在内核态,之后还会告诉你,这样就是边缘触发;
有快递了,打电话告诉你了,可是你没取,所以快递一直放在内核态,但是就不知道快递哪里了,也不再通知你,这样就是水平触发。
select 实际场景(游戏服务器)
python的select()方法直接调用操作系统的IO接口,它监控socket,open files, and pipes(所有带fileno()方法的文件句柄)何时变成readable和writeable,或者通信错误,select()使得同时监控多个连接变得简单,并且这比写一个长循环来等待和监控多客户端连接要高效,因为select直接通过操作系统提供的C的网络接口进程操作,而不是通过python的解释器。
import select import socket import queue server = socket.socket() server.bind(("localhost",9999)) server.listen(1000) # 设置为非阻塞模式,accept等不再阻塞 server.setblocking(False) msg_dict = {} # 有数据的就交给inputs处理,因为自己也是socke,所以自己也加入监控的列表 inputs = [server] outputs = [] while True: # select.select() # readable可读数据 # writeable # exceptional 监测的链接异常的就到这里 readable ,writeable,exceptional = select.select(inputs,outputs,inputs) print(readable ,writeable,exceptional) for r in readable: # 如果是server,代表来了一个新连接 if r is server: conn,addr = server.accept() # print(conn,addr) print("来了个新连接:",addr) # 是因为这个新建立的链接还没有发数据过来,现在就接收的话,程序就报错了。 # 所以要想实现这个客户端发数据来时server端能知道,就需要让select再监测这个conn inputs.append(conn) # 初始化一个队列,后面存要返回给客户端的数据 msg_dict[conn] = queue.Queue() else: # 因为是对应的链接接收数据,所以不能用conn data = r.recv(1024) print("收到数据:", data) # 将要返回客户端的数据写入队列 msg_dict[r].put(data) # r.send(data) # print("发送完毕") # 放入返回的链接队列里 outputs.append(r) # 要返回给客户端链接的列表 for w in writeable: data_to_client = msg_dict[w].get() # 返回给客户端的数据 w.send(data_to_client) # 确保下次循环的时候不要返回旧的链接 outputs.remove(w) # 当客户端断开链接之后 for e in exceptional: # outputs未必有链接,所以需要判断 if e in outputs: outputs.remove(e) # inputs一定有,所以直接移除 inputs.remove(e) # 字典中也要删除 del msg_dict[e]
selectors在linux环境下,会自动调用epoll,在windows环境下回调用select
import selectors import socket sel = selectors.DefaultSelector() def accept(sock,mask): conn,addr = sock.accept() print(conn,addr) # 新连接注册read回调函数,只要连接发过来就调用read函数 sel.register(conn,selectors.EVENT_READ,read) def read(conn,mask): data = conn.recv(1024) # 如果有数据 if data: print(data) # 返回数据给客户端 conn.send(data) else: print("关闭连接:",conn) sel.unregister(conn) conn.close() sock = socket.socket() sock.bind(("localhost",9999)) sock.listen(100) # 设置非阻塞 sock.setblocking(False) # 只要来了一个新连接,就调用accept函数 sel.register(sock,selectors.EVENT_READ,accept) while True: # select看操作系统,linux调用epoll,windows调用select # 默认是阻塞,有活动链接就返回活动的链接列表 events = sel.select() # 循环活动链接列表 for key,mask in events: # 相当于调用accept callback = key.data # key.fileobj就是文件句柄,相当于select例子的r callback(key.fileobj,mask)