进程、线程、协程
进程
1、什么是进程(process)?
定义:1)进程是资源分配最小单位
2)当一个可执行程序被系统执行(分配内存资源)就变成了一个进程
1. 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就称之为进程
2. 程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念
3. 在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。
4. 进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
5. 进程之间有自己独立的内存,各进程之间不能相互访问
6. 创建一个新线程很简单,创建新进程需要对父进程进行复制
多道编程: 在计算机内存中同时存放几道相互独立的程序,他们共享系统资源,相互穿插运行
单道编程: 计算机内存中只允许一个的程序运行
进程并发性:
1)在一个系统中,同时会存在多个进程被加载到内存中,同处于开始到结束之间的状态
2)对于一个单CPU系统来说,程序同时处于运行状态只是一种宏观上的概念
他们虽然都已经开始运行,但就微观而言,任意时刻,CPU上运行的程序只有一个
3)由于操作系统分时,让每个进程都觉得自己独占CPU等资源
注:如果是多核CPU(处理器)实际上是可以实现正在意义的同一时间点有多个线程同时运行
线程并发性:
1)操作系统将时间划分为很多时间段,尽可能的均匀分配给每一个线程。
2)获取到时间片的线程被CPU执行,其他则一直在等待,所以微观上是走走停停,宏观上都在运行。
多核CPU情况:
如果你的程序的线程数少于CPU的核心数,且系统此时没有其他进程同时运行,那么这个程序的每个线程会享有一个CPU,
当同时运行的线程数多于CPU核心数时,CPU会采用一定的调度算法每隔一段时间就将这些线程调入或调出CPU
以确保每个线程都能分享一部分CPU时间,实现多线程并发。
2、有了进程为什么还要线程?
1. 进程优点:
提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率
2. 进程的两个重要缺点
a. 第一点:进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
b. 第二点:进程在执行的过程中如果阻塞,即使进程中有些工作不依赖于输入的数据,也将无法执行(例如等待输入,整个进程就会挂起)。
c. 例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻
即能监听键盘输入、又能监听其它人给你发的消息
d. 你会说,操作系统不是有分时么?分时是指在不同进程间的分时呀
e. 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀
线程
1、什么是线程(thread)(线程是操作系统最小的调度单位)
定义:1)线程是操作系统调度的最小单位
2)它被包含在进程之中,是进程中的实际运作单位
3)进程本身是无法自己执行的,要操作cpu,必须创建一个线程,线程是一系列指令的集合
1. 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
2. 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
3. 无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行
4. 进程本身是无法自己执行的,要操作cpu,必须创建一个线程,线程是一系列指令的集合
5. 所有在同一个进程里的线程是共享同一块内存空间的,不同进程间内存空间不同
6. 同一个进程中的各线程可以相互访问资源,线程可以操作同进程中的其他线程,但进程仅能操作子进程
7. 两个进程想通信,必须要通过一个中间代理
8. 对主线程的修改可能回影响其他子线程,对主进程修改不会影响其他进程因为进程间内存相互独立,但是同一进程下的线程共享内存
2、进程和线程的区别
启动一个线程比启动一个进程快,运行速度没有可比性。
先有一个进程然后才能有线程。
1、进程包含线程
2、线程共享内存空间
3、进程内存是独立的(不可互相访问)
4、进程可以生成子进程,子进程之间互相不能互相访问(相当于在父级进程克隆两个子进程)
5、在一个进程里面线程之间可以交流。两个进程想通信,必须通过一个中间代理来实现
6、创建新线程很简单,创建新进程需要对其父进程进行克隆。
7、一个线程可以控制或操作同一个进程里面的其它线程。但进程只能操作子进程。
8、父进程可以修改不影响子进程,但不能修改。
9、线程可以帮助应用程序同时做几件事
3、进程和程序的区别
1. 程序只是一个普通文件,是一个机器代码指令和数据的集合,所以,程序是一个静态的实体
2. 而进程是程序运行在数据集上的动态过程,进程是一个动态实体,它应创建而产生,应调度执行因等待资
源或事件而被处于等待状态,因完成任务而被撤消
3. 进程是系统进行资源分配和调度的一个独立单位
4.一个程序对应多个进程,一个进程为多个程序服务(两者之间是多对多的关系)
5. 一个程序执行在不同的数据集上就成为不同的进程,可以用进程控制块来唯一地标识每个进程
多线程
Python多线程编程中常用方法:
1、join()方法:如果一个线程或者在函数执行的过程中调用另一个线程,并且希望待其完成操作后才能执行,
那么在调用线程的时就可以使用被调线程的join方法join([timeout]) timeout:可选参数,线程运行的最长时间
2、isAlive()方法:查看线程是否还在运行
3、getName()方法:获得线程名
4、setDaemon()方法:主线程退出时,需要子线程随主线程退出,则设置子线程的setDaemon()
1、线程2种调用方式:直接调用, 继承式调用
import threading import time def sayhi(num): # 定义每个线程要运行的函数 print("running on number:%s" % num) time.sleep(3) #1、target=sayhi :sayhi是定义的一个函数的名字 #2、args=(1,) : 括号内写的是函数的参数 t1 = threading.Thread(target=sayhi, args=(1,)) # 生成一个线程实例 t2 = threading.Thread(target=sayhi, args=(2,)) # 生成另一个线程实例 t1.start() # 启动线程 t2.start() # 启动另一个线程 print(t1.getName()) # 获取线程名 print(t2.getName()) 直接调用
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() 继承式调用
2、for循环同时启动多个线程
说明:下面利用for循环同时启动50个线程并行执行,执行时间是3秒而不是所有线程执行时间的总和
import threading import time def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(3) for i in range(50): t = threading.Thread(target=sayhi,args=('t-%s'%i,)) t.start() for循环启动多个线程
3、t.join(): 实现所有线程都执行结束后再执行主线程
说明:在4中虽然可以实现50个线程同时并发执行,但是主线程不会等待子线程结束在这里我们可以使用t.join()指定等待某个线程结束的结果
import threading import time start_time = time.time() def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(3) t_objs = [] #将进程实例对象存储在这个列表中 for i in range(50): t = threading.Thread(target=sayhi,args=('t-%s'%i,)) t.start() #启动一个线程,程序不会阻塞 t_objs.append(t) print(threading.active_count()) #打印当前活跃进程数量 for t in t_objs: #利用for循环等待上面50个进程全部结束 t.join() #阻塞某个程序 print(threading.current_thread()) #打印执行这个命令进程 print("----------------all threads has finished.....") print(threading.active_count()) print('cost time:',time.time() - start_time) t.join() 主线程等待子线程
4、setDaemon(): 守护线程,主线程退出时,需要子线程随主线程退出
import threading import time start_time = time.time() def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(3) for i in range(50): t = threading.Thread(target=sayhi,args=('t-%s'%i,)) t.setDaemon(True) #把当前线程变成守护线程,必须在t.start()前设置 t.start() #启动一个线程,程序不会阻塞 print('cost time:',time.time() - start_time) 守护线程
注:因为刚刚创建的线程是守护线程,所以主线程结束后子线程就结束了,运行时间不是3秒而是0.01秒
5、GIL锁和用户锁(Global Interpreter Lock 全局解释器锁
)
1.全局解释器锁:保证同一时间仅有一个线程对资源有操作权限
作用:在一个进程内,同一时刻只能有一个线程通过GIL锁 被CUP调用,切换条件:I/O操作、固定时间(系统决定)
说明:python多线程中GIL锁只是在CPU操作时(如:计算)才是串行的,其他都是并行的,所以比串行快很多
1)为了解决不同线程同时访问同一资源时,数据保护问题,而产生了GIL
2)GIL在解释器的层面限制了程序在同一时间只有一个线程被CPU实际执行,而不管你的程序里实际开了多少条线程
3)为了解决这个问题,CPython自己定义了一个全局解释器锁,同一时间仅仅有一个线程可以拿到这个数据
4)python之所以会产生这种不好的状况是因为python启用一个线程是调用操作系统原生线程,就是C接口
5)但是这仅仅是CPython这个版本的问题,在PyPy,中就没有这种缺陷
2. 用户锁:线程锁(互斥锁Mutex) :当前线程还未操作完成前其他所有线程都无法对其操作,即使已经释放了GIL锁
1. 在有GIL锁时为何还需要用户锁
1)GIL锁只能保证同一时间只能有一个线程对某个资源操作,但当上一个线程还未执行完毕时可能就会释放GIL,其他线程就可以操作了
2. 线程锁的原理
1)当一个线程对某个资源进行CPU计算的操作时加一个线程锁,只有当前线程计算完成主动释放锁,其他线程才能对其操作
2)这样就可以防止还未计算完成,释放GIL锁后其他线程对这个资源操作导致混乱问题
import time import threading lock = threading.Lock() #1 生成全局锁 def addNum(): global num #2 在每个线程中都获取这个全局变量 print('--get num:',num ) time.sleep(1) lock.acquire() #3 修改数据前加锁 num -= 1 #4 对此公共变量进行-1操作 lock.release() #5 修改后释放 用户锁使用举例
3. 在有GIL的情况下执行 count = count + 1 会出错原因解析,用线程锁解决方法
# 1)第一步:count = 0 count初始值为0 # 2)第二步:线程1要执行对count加1的操作首先申请GIL全局解释器锁 # 3)第三步:调用操作系统原生线程在操作系统中执行 # 4)第四步:count加1还未执行完毕,时间到了被要求释放GIL # 5)第五步:线程1释放了GIL后线程2此时也要对count进行操作,此时线程1还未执行完,所以count还是0 # 6)第六步:线程2此时拿到count = 0后也要对count进行加1操作,假如线程2执行很快,一次就完成了 # count加1的操作,那么count此时就从0变成了1 # 7)第七步:线程2执行完加1后就赋值count=1并释放GIL # 8)第八步:线程2执行完后cpu又交给了线程1,线程1根据上下文继续执行count加1操作,先拿到GIL # 锁,完成加1操作,由于线程1先拿到的数据count=0,执行完加1后结果还是1 # 9)第九步:线程1将count=1在次赋值给count并释放GIL锁,此时连个线程都对数据加1,但是值最终是1 出错原因分析
1、使用线程锁解决上面问题的原理
1) 在GIL锁中再加一个线程锁,线程锁是用户层面的锁
2) 线程锁就是一个线程在对数据操作前加一把锁,防止其他线程复制或者操作这个数据
3) 只有这个线程对数据操作完毕后才会释放这个锁,其他线程才能操作这个数据
2、定义一个线程锁非常简单只用三步:
第一步: lock = threading.Lock() #定义一把锁
第二步: lock.acquire() #对数据操作前加锁防止数据被另一线程操作
第三步: lock.release() #对数据操作完成后释放锁
6、死锁
1. 死锁定义
两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
2. 死锁举例
1. 启动5个线程,执行run方法,假如thread1首先抢到了A锁,此时thread1没有释放A锁,紧接着执行代码mutexB.acquire(),抢到了B锁,
在抢B锁时候,没有其他线程与thread1争抢,因为A锁没有释放,其他线程只能等待
2. thread1执行完func1函数,然后执行func2函数,此时thread1拿到B锁,然后执行time.sleep(2),此时不会释放B锁
3. 在thread1执行func2的同时thread2开始执行func1获取到了A锁,然后继续要获取B锁
4. 不幸的是B锁还被thread1占用,thread1占用B锁时还需要同时获取A锁才能向下执行,但是此时发现A锁已经被thread2暂用,这样就死锁了
from threading import Thread,Lock import time mutexA=Lock() mutexB=Lock() class MyThread(Thread): def run(self): self.func1() self.func2() def func1(self): mutexA.acquire() print('\033[41m%s 拿到A锁\033[0m' %self.name) mutexB.acquire() print('\033[42m%s 拿到B锁\033[0m' %self.name) mutexB.release() mutexA.release() def func2(self): mutexB.acquire() print('\033[43m%s 拿到B锁\033[0m' %self.name) time.sleep(2) mutexA.acquire() print('\033[44m%s 拿到A锁\033[0m' %self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(2): t=MyThread() t.start() # 运行结果:输出下面结果后程序卡死,不再向下进行了 # Thread-1 拿到A锁 # Thread-1 拿到B锁 # Thread-1 拿到B锁 # Thread-2 拿到A锁 产生死锁代码
7、递归锁:lock = threading.RLock() 解决死锁问题
1. 递归锁的作用是同一线程中多次请求同一资源,但是不会参数死锁。
2. 这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。
3. 直到一个线程所有的acquire都被release,其他的线程才能获得资源。
from threading import Thread,Lock,RLock import time mutexA=mutexB=RLock() class MyThread(Thread): def run(self): self.f1() self.f2() def f1(self): mutexA.acquire() print('%s 拿到A锁' %self.name) mutexB.acquire() print('%s 拿到B锁' %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(5): t=MyThread() t.start() # 下面是运行结果:不会产生死锁 # Thread-1 拿到A锁 # Thread-1 拿到B锁 # Thread-1 拿到B锁 # Thread-1 拿到A锁 # Thread-2 拿到A锁 # Thread-2 拿到B锁 # Thread-2 拿到B锁 # Thread-2 拿到A锁 # Thread-4 拿到A锁 # Thread-4 拿到B锁 # Thread-4 拿到B锁 # Thread-4 拿到A锁 # Thread-3 拿到A锁 # Thread-3 拿到B锁 # Thread-3 拿到B锁 # Thread-3 拿到A锁 # Thread-5 拿到A锁 # Thread-5 拿到B锁 # Thread-5 拿到B锁 # Thread-5 拿到A锁 如果使用RLock代替Lock,则不会发生死锁
8、Semaphore(信号量)
1. 互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据
2. 比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去
3. 作用就是同一时刻允许运行的线程数量
# import threading,time # def run(n): # semaphore.acquire() # time.sleep(1) # print("run the thread: %s\n" %n) # semaphore.release() # # if __name__ == '__main__': # semaphore = threading.BoundedSemaphore(5) #最多允许5个线程同时运行 # for i in range(22): # t = threading.Thread(target=run,args=(i,)) # t.start() # # while threading.active_count() != 1: # pass #print threading.active_count() # else: # print('----all threads done---') # 代码结果说明:这里可以清晰看到运行时0-4是同时运行的没有顺序,而且是前五个, # 表示再semaphore这个信号量的定义下程序同时仅能执行5个线程 信号量举例
9、events总共就只有四个方法
1. event.set() : 设置标志位
2. event.clear() : 清除标志位
3. event.wait() : 等待标志被设定
4. event.is_set() : 判断标志位是否被设定
import time,threading event = threading.Event() #第一:写一个红绿灯的死循环 def lighter(): count = 0 event.set() #1先设置为绿灯 while True: if count > 5 and count <10: #2改成红灯 event.clear() #3把标志位清了 print("red light is on.....") elif count > 10: event.set() #4再设置标志位,变绿灯 count = 0 else: print("green light is on.....") time.sleep(1) count += 1 #第二:写一个车的死循环 def car(name): while True: if event.is_set(): #设置了标志位代表绿灯 print("[%s] is running"%name) time.sleep(1) else: print('[%s] sees red light, waiting......'%name) event.wait() print('[%s] green light is on,start going.....'%name) light = threading.Thread(target=lighter,) light.start() car1 = threading.Thread(target=car,args=("Tesla",)) car1.start() events(红绿灯例子)
协程
1、什么是协程(进入上一次调用的状态)
1. 协程,又称微线程,纤程,协程是一种用户态的轻量级线程。
2. 线程的切换会保存到CPU的栈里,协程拥有自己的寄存器上下文和栈,
3. 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈
4. 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态
5. 协程最主要的作用是在单线程的条件下实现并发的效果,但实际上还是串行的(像yield一样)
2、协程的好处
1. 无需线程上下文切换的开销(可以理解为协程切换就是在不同函数间切换,不用像线程那样切换上下文CPU)
2. 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突
3. 用法:最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
3、协程缺点
1. 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上
2. 线程阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
4、使用yield实现协程相同效果
import time import queue def consumer(name): print("--->starting eating baozi...") while True: new_baozi = yield # 只要遇到yield程序就返回,yield还可以接收数据 print("[%s] is eating baozi %s" % (name, new_baozi)) time.sleep(1) def producer(): r = con.__next__() # 直接调用消费者的__next__方法 r = con2.__next__() # 函数里面有yield第一次加括号调用会变成一个生成器函数不执行,运行next才执行 n = 0 while n < 5: n += 1 con.send(n) # send恢复生成器同时并传递一个值给yield con2.send(n) print("\033[32;1m[producer]\033[0m is making baozi %s" % n) if __name__ == '__main__': con = consumer("c1") con2 = consumer("c2") p = producer() yield模拟实现协程效果
5、协程为何能处理大并发1:Greenlet遇到I/O手动切换
1. 协程之所以快是因为遇到I/O操作就切换(最后只有CPU运算)
2. 这里先演示用greenlet实现手动的对各个协程之间切换
3. 其实Gevent模块仅仅是对greenlet的再封装,将I/O间的手动切换变成自动切换
from greenlet import greenlet def test1(): print(12) #4 gr1会调用test1()先打印12 gr2.switch() #5 然后gr2.switch()就会切换到gr2这个协程 print(34) #8 由于在test2()切换到了gr1,所以gr1又从上次停止的位置开始执行 gr2.switch() #9 在这里又切换到gr2,会再次切换到test2()中执行 def test2(): print(56) #6 启动gr2后会调用test2()打印56 gr1.switch() #7 然后又切换到gr1 print(78) #10 切换到gr2后会接着上次执行,打印78 gr1 = greenlet(test1) #1 启动一个协程gr1 gr2 = greenlet(test2) #2 启动第二个协程gr2 gr1.switch() #3 首先gr1.switch() 就会去执行gr1这个协程 Greenlet遇到I/O手动切换
6、协程为何能处理大并发2:Gevent遇到I/O自动切换
1. Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程
2. 在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程
3. Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
4. Gevent原理是只要遇到I/O操作就会自动切换到下一个协程
7、Gevent实现简单的自动切换小例子
注:在Gevent模仿I/O切换的时候,只要遇到I/O就会切换,哪怕gevent.sleep(0)也要切换一次
import gevent def func1(): print('\033[31;1m第一次打印\033[0m') gevent.sleep(2) # 为什么用gevent.sleep()而不是time.sleep()因为是为了模仿I/O print('\033[31;1m第六次打印\033[0m') def func2(): print('\033[32;1m第二次打印\033[0m') gevent.sleep(1) print('\033[32;1m第四次打印\033[0m') def func3(): print('\033[32;1m第三次打印\033[0m') gevent.sleep(1) print('\033[32;1m第五次打印\033[0m') gevent.joinall([ # 将要启动的多个协程放到event.joinall的列表中,即可实现自动切换 gevent.spawn(func1), # gevent.spawn(func1)启动这个协程 gevent.spawn(func2), gevent.spawn(func3), ]) # 运行结果: # 第一次打印 # 第二次打印 # 第三次打印 # 第四次打印 # 第五次打印 # 第六次打印 Gevent实现简单的自动切换小例子
8、使用Gevent实现并发下载网页与串行下载网页时间比较
from urllib import request import gevent,time from gevent import monkey monkey.patch_all() #把当前程序所有的I/O操作给我单独做上标记 def f(url): print('GET: %s' % url) resp = request.urlopen(url) data = resp.read() print('%d bytes received from %s.' % (len(data), url)) #1 并发执行部分 time_binxing = 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()-time_binxing) #2 串行部分 time_chuanxing = time.time() urls = [ 'https://www.python.org/', 'https://www.yahoo.com/', 'https://github.com/', ] for url in urls: f(url) print("串行时间:",time.time()-time_chuanxing) # 注:为什么要在文件开通使用monkey.patch_all() # 1. 因为有很多模块在使用I / O操作时Gevent是无法捕获的,所以为了使Gevent能够识别出程序中的I / O操作。 # 2. 就必须使用Gevent模块的monkey模块,把当前程序所有的I / O操作给我单独做上标记 # 3.使用monkey做标记仅用两步即可: 第一步(导入monkey模块): from gevent import monkey 第二步(声明做标记) : monkey.patch_all() 并行串行时间比较
说明:monkey.patch_all()猴子补丁作用
1)用过gevent就会知道,会在最开头的地方gevent.monkey.patch_all();
2)作用是把标准库中的thread/socket等给替换掉.这样我们在后面使用socket的时候可以跟平常一样使用,无需修改任何代码,但是它变成非阻塞的了.
9、通过gevent自己实现单线程下的多socket并发
import gevent from gevent import socket,monkey #下面使用的socket是Gevent的socket,实际测试monkey没用 # monkey.patch_all() def server(port): s = socket.socket() s.bind(('0.0.0.0',port)) s.listen(5) while True: cli,addr = s.accept() gevent.spawn(handle_request,cli) def handle_request(conn): try: while True: data = conn.recv(1024) print('recv:',data) conn.send(data) if not data: conn.shutdown(socket.SHUT_WR) except Exception as e: print(e) finally: conn.close() if __name__=='__main__': server(8001) server端
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").strip() if len(msg) == 0:continue s.sendall(msg) data = s.recv(1024) print('Received', repr(data)) s.close() client端
10、协程本质原理
1. 协程1通过os去读一个file,这个时候就是一个io操作,在调用os的接口前,就会有一个列表
2. 协程1的这个操作就会被注册到这个列表中,然后就切换到其他协程去处理;
3. 等待os拿到要读file后,也会把这个文件句柄放在这个列表中
4. 然后等待在切换到协程1的时候,协程1就可以直接从列表中拿到数据,这样就可以实现不阻塞了
5. epoll返回给协程的任务列表在内核态,协程在用户态,用户态协程是不能直接访问内核态的任务列表的,
所以需要拷贝整个内核态的任务列表到用户态,供协程去访问和查询
11、epoll处理 I/O 请求原理
1. epoll() 中内核则维护一个链表,epoll_wait 直接检查链表是不是空就知道是否有文件描述符准备好了。
2. 在内核实现中 epoll 是根据每个 sockfd 上面的与设备驱动程序建立起来的回调函数实现的。
3. 某个 sockfd 上的事件发生时,与它对应的回调函数就会被调用,来把这个 sockfd 加入链表,其他处于“空闲的”状态的则不会。
4. epoll上面链表中获取文件描述,这里使用内存映射(mmap)技术, 避免了复制大量文件描述符带来的开销
内存映射(mmap):内存映射文件,是由一个文件到一块内存的映射,将不必再对文件执行I/O操作
12、select处理协程
1. 拷贝所有的文件描述符给协程,不论这些任务的是否就绪,都会被返回
2. 那么协程就只能for循环去查找自己的文件描述符,也就是任务列表,select的兼容性非常好,支持linux和windows