Python 三程三器的那些事
装饰器
1、什么是装饰器
- 装饰器本质是函数,用来给其他函数添加新的功能
- 特点:不修改调用方式、不修改源代码
2、装饰器的作用
- 装饰器作用:本质是函数(装饰其他函数)就是为其他函数添加其他功能
- 装饰器必须准寻得原则:
- 不能修改被装饰函数的源代码、不能修改被装饰函数的调用方式
- 实现装饰器知识储备:
- 函数即“变量”
- 高阶函数
- 嵌套函数 高阶函数+潜逃函数=》装饰器
3、使用高阶函数模仿装饰器功能
1.定义:把一个函数名当做实参传给另一个函数
2.返回值中包含函数名
3.下面使用高阶函数虽然可以实现装饰器的一些功能,但是违反了装饰器不能改变调用方式的原则,
以前使用bar()现在将调用方式改编成了test1(bar)就是将bar的函数名当做变量传给了test1()
#! /usr/bin/env python # -*- coding: utf-8 -*- import time def timer(func): start_time = time.time() func() print '函数执行时间为', time.time() - start_time def test(): print '开始执行test' time.sleep(3) print 'test执行结束' timer(test) ''' 开始执行test test执行结束 函数执行时间为 3.00332999229 '''
4.高阶函数——不修改高阶函数的调用方式增加新的功能(但是无法传参数)
注:bar = test2(bar) 等价于:@timer重新将函数名bar赋值,将原函数bar的内存地址当做实参传递该函数test2(),再将test2()赋值给bar
import time def bar(): time.sleep(3) print("in the bar") def test2(func): print(func) return func bar = test2(bar) bar()
5.嵌套函数
嵌套函数:在一个函数中嵌套另一个函数,并在函数内部调用
def foo(): print("in the foo") def bar(): print("in the bar") bar() foo()
4、能够适应90%的业务需求
- 在装饰器中 @timer等价于 test1=timer(test1)
- 在timer()函数中返回值是return deco
- 所以timer(test1)作用是将函数test1内存地址当做参数传递给timer()
- timer() 函数最后将运行后的函数deco内存地址作为返回值返回
- test1=timer(test1)作用就是将将deco函数内存地址赋值给test1,所以最后运行test1()就相当于运行deco()
- 所以最后调用时给test2()传入参数就相当于给deco传入参数
import time def timer(func): #timer(test1) func=test1 def deco(*args,**kwargs): start_time = time.time() func(*args,**kwargs) #run test1 stop_time = time.time() print("running time is %s"%(stop_time-start_time)) return deco @timer # test1=timer(test1) def test1(): time.sleep(3) print("in the test1") @timer def test2(name): print("in the test2",name) test1() test2("tom")
5、对特定网页进行身份验证
import time user,passwd = 'aaa','123' def auth(func): def wrapper(*args,**kwargs): username = input("Username:").strip() password = input("Password:").strip() if user == username and password == passwd: print("User has passed authentication") res = func(*args,**kwargs) #这里执行func()相当于执行调用的函数如home() return res #为了获得home()函数返回值,可以将执行结果赋值给res然后返回print(home())结果是"from home"而不是"None"了 else: exit("Invalid username or password") return wrapper def index(): print("welcome to index page") @auth def home(): print("welcome to home page") return "from home" @auth def bbs(): print("welcome to bbs page") index() print(home()) #在这里调用home()相当于调用wrapper() bbs()
6、实现对不同网页不同方式的身份认证
- @auth(auth_type="local")代码作用
- 在上面的代码中使用@auth相当于先将home函数的内存地址当做变量传入auth()函数,执行结果后home()相当于wrapper()
- 而在这里验证的时候犹豫@auth(auth_type="local")中有()括号,那么就相当于将执行auth()函数而且是将auth_type="local当做参数传入到auth()函数执行
- 所以outer_wrapper函数也会执行,outer_wrapper函数的执行结果返回的就是wrapper()函数的内存地址
- 所以最终结果同样是执行home()函数就相当于执行wrapper函数
- 但是有所不同的是着这个版本中我们可以在外层的auth函数中传入新的参数帮组我们根据需求判断
import time user,passwd = 'aaa','123' def auth(auth_type): print("auth func:",auth_type) def outer_wrapper(func): def wrapper(*args, **kwargs): print("wrapper func args:", *args, **kwargs) if auth_type == "local": username = input("Username:").strip() password = input("Password:").strip() if user == username and passwd == password: print("\033[32;1mUser has passed authentication\033[0m") res = func(*args, **kwargs) # from home print("---after authenticaion ") return res else: exit("\033[31;1mInvalid username or password\033[0m") elif auth_type == "ldap": print("搞毛线ldap,不会。。。。") return wrapper return outer_wrapper def index(): print("welcome to index page") @auth(auth_type="local") # home = wrapper() def home(): print("welcome to home page") return "from home" @auth(auth_type="ldap") def bbs(): print("welcome to bbs page") index() print(home()) #wrapper() bbs()
#! /usr/bin/env python # -*- coding: utf-8 -*- import time def auth(auth_type): print("auth func:",auth_type) def outer_wrapper(func): def wrapper(*args, **kwargs): print("wrapper func args:", *args, **kwargs) print('运行前') func(*args, **kwargs) print('运行后') return wrapper return outer_wrapper @auth(auth_type="local") # home = wrapper() def home(): print("welcome to home page") return "from home" home()
7、使用闭包实现装饰器功能
闭包概念:
- 在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量,并且外函数的返回值是内函数的引用,这样就构成了一个闭包
- 一般情况下,在我们认知当中,如果一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量都会消失。
- 但是闭包是一种特殊情况,如果外函数在结束的时候发现有自己的临时变量将来会在内部函数中用到,就把这个临时变量绑定给了内部函数,然后自己再结束。
#! /usr/bin/env python # -*- coding: utf-8 -*- import time def timer(func): #timer(test1) func=test1 def deco(*args,**kwargs): # # 函数嵌套 start_time = time.time() func(*args,**kwargs) # 跨域访问,引用了外部变量func (func实质是函数内存地址) stop_time = time.time() print "running time is %s"%(stop_time-start_time) return deco # 内层函数作为外层函数返回值 def test(name): print "in the test2",name time.sleep(2) test = timer(test) # 等价于 ==》 @timer语法糖 test("tom") ''' 运行结果: in the test2 tom running time is 2.00302696228 '''
生成器
1、什么是生成器
- 生成器就是一个特殊的迭代器
- 一个有yield关键字的函数就是一个生成器
- 生成器是这样一个函数,它记住上一次返回时在函数体中的位置。
- 对生成器函数的第二次(或第 n 次)调用跳转至该函数中间,而上次调用的所有局部变量都保持不变。
2、定义
- 生成器,即生成一个容器。
- 在Python中,一边循环,一边计算的机制,称为生成器。
- 生成器可以理解为一种数据类型,这种数据类型自动实现了迭代器协议(其他数据类型需要调用自己的内置iter()方法或__iter__()的内置函数),
- 所以,生成器就是一个可迭代对象。
3、生成器哪些场景应用
- 生成器是一个概念,我们平常写代码可能用的并不多,但是python源码大量使用
- 比如我们tornado框架就是基于 生成器+协程
- 在我们代码中使用举例
- 比如我们要生成一百万个数据,如果用生成器非常节省空间,用列表浪费大量空间
import time t1 = time.time() g = (i for i in range(100000000)) t2 = time.time() lst = [i for i in range(100000000)] t3 = time.time() print('生成器时间:',t2 - t1) # 生成器时间: 0.0 print('列表时间:',t3 - t2) # 列表时间: 5.821957349777222
4、生成器的作用
- 通过列表生成式,我们可以直接创建一个列表,但是,受到内存限制,列表容量肯定是有限的。
- 而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
- 所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?
- 这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
- 要创建一个generator,有很多种方法,第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator:
print( [i*2 for i in range(10)] ) #列表生成式: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] print( (i*2 for i in range(10)) ) #生 成 器: <generator object <genexpr> at 0x005A3690>
- 我们可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?
- 如果要一个一个打印出来,可以通过next()函数获得generator的下一个返回值:
g = (i*2 for i in range(10)) print( g.__next__() ) # 0 print( g.__next__() ) # 2
5、生成器工作原理
- 生成器是这样一个函数,它记住上一次返回时在函数体中的位置。
- 对生成器函数的第二次(或第 n 次)调用跳转至该函数中间,而上次调用的所有局部变量都保持不变。
- 生成器不仅“记住”了它数据状态;生成器还“记住”了它在流控制构造(在命令式编程中,这种构造不只是数据值)中的位置。
- 生成器是一个函数,而且函数的参数都会保留。
- 迭代到下一次的调用时,所使用的参数都是第一次所保留下的,即是说,在整个所有函数调用的参数都是第一次所调用时保留的,而不是新创建的
6、yield生成器运行机制
- 在Python中,yield就是这样的一个生成器。
- 当你问生成器要一个数时,生成器会执行,直至出现 yield 语句,生成器把yield 的参数给你,之后生成器就不会往下继续运行。
- 当你问他要下一个数时,他会从上次的状态开始运行,直至出现yield语句,把参数给你,之后停下。如此反复
- 在python中,当你定义一个函数,使用了yield关键字时,这个函数就是一个生成器
- 它的执行会和其他普通的函数有很多不同,函数返回的是一个对象,而不是你平常所用return语句那样,能得到结果值。如果想取得值,那得调用next()函数
- 每当调用一次迭代器的next函数,生成器函数运行到yield之处,返回yield后面的值且在这个地方暂停,所有的状态都会被保持住,直到下次next函数被调用,或者碰到异常循环退出。
def fib(max_num): a,b = 1,1 while a < max_num: yield b a,b=b,a+b g = fib(10) #生成一个生成器:[1,2, 3, 5, 8, 13] print(g.__next__()) #第一次调用返回:1 print(list(g)) #把剩下元素变成列表:[2, 3, 5, 8, 13]
7、yield实现单线程下的并发效果
- yield相当于 return 返回一个值,并且记住这个返回的位置,下次迭代时,代码从yield的下一条语句开始执行。
- send() 和next()一样,都能让生成器继续往下走一步(下次遇到yield停),但send()能传一个值,这个值作为yield表达式整体的结果
def consumer(name): print("%s 准备吃包子啦!" %name) while True: baozi = yield print("包子[%s]来了,被[%s]吃了!" %(baozi,name)) c = consumer("Tom") c.__next__() b1 = "韭菜馅包子" c.send(b1) # c.send(b1)作用: # c.send()的作用是给yied的传递一个值,并且每次调用c.send()的同时自动调用一次__next__ '''运行结果: Tom 准备吃包子啦! 包子[韭菜馅包子]来了,被[Tom]吃了! '''
import time def consumer(name): print("%s 准备吃包子啦!" %name) while True: baozi = yield print("包子[%s]来了,被[%s]吃了!" %(baozi,name)) def producer(name): c = consumer('A') c2 = consumer('B') c.__next__() c2.__next__() print("老子开始准备做包子啦!") for i in range(10): time.sleep(1) print("做了2个包子!") c.send(i) c2.send(i) producer("alex") '''运行结果: A 准备吃包子啦! B 准备吃包子啦! 老子开始准备做包子啦! 做了2个包子! 包子[0]来了,被[A]吃了! 包子[0]来了,被[B]吃了! 做了2个包子! 包子[1]来了,被[A]吃了! 包子[1]来了,被[B]吃了! 做了2个包子! 包子[2]来了,被[A]吃了! 包子[2]来了,被[B]吃了! 做了2个包子! 包子[3]来了,被[A]吃了! 包子[3]来了,被[B]吃了! 做了2个包子! 包子[4]来了,被[A]吃了! 包子[4]来了,被[B]吃了! 做了2个包子! 包子[5]来了,被[A]吃了! 包子[5]来了,被[B]吃了! '''
迭代器
1、什么是迭代器
- 迭代器是访问集合内元素的一种方法
- 总是从集合内第一个元素访问,直到所有元素都被访问过结束,当调用 __next__而元素返回会引发一个,StopIteration异常
- 有两个方法:_iter_ _next_
- _iter_ : 返回迭代器自身
- _next_: 返回下一个元素
2、定义:
- 迭代器是访问集合内元素的一种方式。迭代器对象从集合的第一个元素开始访问,直到所有的元素都被访问一遍后结束。
3、迭代器和可迭代对象
- 凡是可作用于
for
循环的对象都是可迭代的(Iterable)类型; - 凡是可作用于
next()
函数的对象都是迭代器(Iterator)
类型,它们表示一个惰性计算的序列; - 集合数据类型如
list
、dict
、str
等是可迭代的但不是迭代器,不过可以通过iter()
函数获得一个Iterator
对象。 - Python的
for
循环本质上就是通过不断调用next()
函数实现的 - 总结: 一个实现了__iter__方法的对象是可迭代的,一个实现next方法的对象是迭代器
4、迭代器的两个方法
- 迭代器仅是一容器对象,它实现了迭代器协议。它有两个基本方法
- __next__方法:返回容器的下一个元素
- __iter__方法:返回迭代器自身
- 迭代器是访问集合内元素的一种方式。迭代器对象从集合的第一个元素开始访问,直到所有的元素都被访问一遍后结束。
- __iter__方法会返回一个迭代器(iterator),所谓的迭代器就是具有next方法的对象。
- 在调用next方法时,迭代器会返回它的下一个值,如果next方法被调用,但迭代器中没有值可以返就会引发一个StopIteration异常
a = iter([1,2,]) #生成一个迭代器 print(a.__next__()) print(a.__next__()) print(a.__next__()) #在这一步会引发 “StopIteration” 的异常
5、判断是迭代器和可迭代对象
注:列表,元组,字典是可迭代的但不是迭代器
from collections import Iterable print(isinstance([],Iterable)) #True print(isinstance({},Iterable)) #True print(isinstance((),Iterable)) #True print(isinstance("aaa",Iterable)) #True print(isinstance((x for x in range(10)),Iterable)) #True
6、列表不是迭代器,只有生成器是迭代器
from collections import Iterator t = [1,2,3,4] print(isinstance(t,Iterator)) #False t1 = iter(t) print(isinstance(t1,Iterator)) #True
7、自定义迭代器
#! /usr/bin/env python # -*- coding: utf-8 -*- class MyRange(object): def __init__(self, n): self.idx = 0 self.n = n def __iter__(self): return self def next(self): if self.idx < self.n: val = self.idx self.idx += 1 return self.n[val] else: raise StopIteration() l = [4,5,6,7,8] obj = MyRange(l) print obj.next() # 4 print obj.next() # 5 print obj.next() # 6
8、迭代器与生成器
#! /usr/bin/env python # -*- coding: utf-8 -* l = [1,2,3,4,5] # 列表是一个可迭代对象,不是一个迭代器 print dir(l) # 所以 l 中有 __iter__() 方法,没有 __next__()方法 iter_obj = l.__iter__() # __iter__()方法返回迭代器对象本身(这个迭代器对象就会有 next 方法了) print '###################################\n' print iter_obj.next() # 1 print iter_obj.next() # 2 print iter_obj.next() # 3
进程与线程的简介
1、什么是进程(process)?(进程是资源集合)
2、进程是资源分配的最小单位( 内存、cpu、网络、io)
3、一个运行起来的程序就是一个进程
- 什么是程序(程序是我们存储在硬盘里的代码)
- 硬盘(256G)、内存条(8G)
- 当我们双击图标,打开程序的时候,实际上就是通过I/O操作(读写)内存条里面
- 内存条就是我们所指的资源
CPU分时
- CPU比你的手速快多了,分时处理每个线程,但是由于太快然你觉得每个线程都是独占cpu
- cpu是计算,只有时间片到了,获取cpu,线程真正执行
- 当你想使用 网络、磁盘等资源的时候,需要cpu的调度
- 进程具有独立的内存空间,所以没有办法相互通信
进程如何通信
- 进程queue(父子进程通信)
- pipe(同一程序下两个进程通信)
- managers(同一程序下多个进程通信)
- RabbitMQ、redis等(不同程序间通信)
为什么需要进程池
- 一次性开启指定数量的进程
- 如果有十个进程,有一百个任务,一次可以处理多少个(一次性只能处理十个)
- 防止进程开启数量过多导致服务器压力过大
2、定义:进程是资源分配最小单位
- 当一个可执行程序被系统执行(分配内存资源)就变成了一个进程
- 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就称之为进程
- 程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念
- 在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。
- 进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
- 进程之间有自己独立的内存,各进程之间不能相互访问
- 创建一个新线程很简单,创建新进程需要对父进程进行复制
- 多道编程: 在计算机内存中同时存放几道相互独立的程序,他们共享系统资源,相互穿插运行
- 单道编程: 计算机内存中只允许一个的程序运行
3、进程并发性:
- 在一个系统中,同时会存在多个进程被加载到内存中,同处于开始到结束之间的状态
- 对于一个单CPU系统来说,程序同时处于运行状态只是一种宏观上的概念
- 他们虽然都已经开始运行,但就微观而言,任意时刻,CPU上运行的程序只有一个
- 由于操作系统分时,让每个进程都觉得自己独占CPU等资源
- 注:如果是多核CPU(处理器)实际上是可以实现正在意义的同一时间点有多个线程同时运行
4、线程并发性:
- 操作系统将时间划分为很多时间段,尽可能的均匀分配给每一个线程。
- 获取到时间片的线程被CPU执行,其他则一直在等待,所以微观上是走走停停,宏观上都在运行。
- 多核CPU情况:
- 如果你的程序的线程数少于CPU的核心数,且系统此时没有其他进程同时运行,那么这个程序的每个线程会享有一个CPU,
- 当同时运行的线程数多于CPU核心数时,CPU会采用一定的调度算法每隔一段时间就将这些线程调入或调出CPU
- 以确保每个线程都能分享一部分CPU时间,实现多线程并发。
- 多核CPU情况:
5、有了进程为什么还要线程?
1.进程优点:
- 提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率
2. 进程的两个重要缺点
- 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
- 进程在执行的过程中如果阻塞,即使进程中有些工作不依赖于输入的数据,也将无法执行(例如等待输入,整个进程就会挂起)。
- 例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻 即能监听键盘输入、又能监听其它人给你发的消息
- 你会说,操作系统不是有分时么?分时是指在不同进程间的分时呀
- 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀
6、什么是线程(thread)(线程是操作系统最小的调度单位)
- 定义:
- 线程是操作系统调度的最小单位
- 它被包含在进程之中,是进程中的实际运作单位
- 进程本身是无法自己执行的,要操作cpu,必须创建一个线程,线程是一系列指令的集合
- 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
- 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
- 无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行
- 进程本身是无法自己执行的,要操作cpu,必须创建一个线程,线程是一系列指令的集合
- 所有在同一个进程里的线程是共享同一块内存空间的,不同进程间内存空间不同
- 同一个进程中的各线程可以相互访问资源,线程可以操作同进程中的其他线程,但进程仅能操作子进程
- 两个进程想通信,必须要通过一个中间代理
- 对主线程的修改可能回影响其他子线程,对主进程修改不会影响其他进程因为进程间内存相互独立,但是同一进程下的线程共享内存
7、进程和线程的区别
- 启动一个线程比启动一个进程快,运行速度没有可比性。
- 先有一个进程然后才能有线程。
- 进程包含线程
- 线程共享内存空间
- 进程内存是独立的(不可互相访问)
- 进程可以生成子进程,子进程之间互相不能互相访问(相当于在父级进程克隆两个子进程)
- 在一个进程里面线程之间可以交流。两个进程想通信,必须通过一个中间代理来实现
- 创建新线程很简单,创建新进程需要对其父进程进行克隆。
- 一个线程可以控制或操作同一个进程里面的其它线程。但进程只能操作子进程。
- 父进程可以修改不影响子进程,但不能修改。
- 线程可以帮助应用程序同时做几件事
8、进程和程序的区别
- 程序只是一个普通文件,是一个机器代码指令和数据的集合,所以,程序是一个静态的实体
- 而进程是程序运行在数据集上的动态过程,进程是一个动态实体,它应创建而产生,应调度执行因等待资 源或事件而被处于等待状态,因完成任务而被撤消
- 进程是系统进行资源分配和调度的一个独立单位
- 一个程序对应多个进程,一个进程为多个程序服务(两者之间是多对多的关系)
- 一个程序执行在不同的数据集上就成为不同的进程,可以用进程控制块来唯一地标识每个进程
多线程
Python多线程编程中常用方法:
- join()方法:如果一个线程或者在函数执行的过程中调用另一个线程,并且希望待其完成操作后才能执行,那么在调用线程的时就可以使用被调线程的join方法join([timeout]) timeout:可选参数,线程运行的最长时间
- isAlive()方法:查看线程是否还在运行
- getName()方法:获得线程名
- setDaemon()方法:主线程退出时,需要子线程随主线程退出,则设置子线程的setDaemon()
GIL全局解释器锁:
- 在python全局解释器下,保证同一时间只有一个线程运行
- 防止多个线程都修改数据
线程锁(互斥锁):
- GIL锁只能保证同一时间只能有一个线程对某个资源操作,但当上一个线程还未执行完毕时可能就会释放GIL,其他线程就可以操作了
- 线程锁本质把线程中的数据加了一把互斥锁
- mysql中共享锁 & 互斥锁
- mysql共享锁:共享锁,所有线程都能读,而不能写
- mysql排它锁:排它,任何线程读取这个这个数据的权利都没有
- 加上线程锁之后所有其他线程,读都不能读这个数据
- 有了GIL全局解释器锁为什么还需要线程锁
- 因为cpu是分时使用的
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()
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)
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 全局解释器锁)
- 全局解释器锁:保证同一时间仅有一个线程对资源有操作权限
- 作用:在一个进程内,同一时刻只能有一个线程通过GIL锁 被CUP调用,切换条件:I/O操作、固定时间(系统决定)
- 说明:python多线程中GIL锁只是在CPU操作时(如:计算)才是串行的,其他都是并行的,所以比串行快很多
- 为了解决不同线程同时访问同一资源时,数据保护问题,而产生了GIL
- GIL在解释器的层面限制了程序在同一时间只有一个线程被CPU实际执行,而不管你的程序里实际开了多少条线程
- 为了解决这个问题,CPython自己定义了一个全局解释器锁,同一时间仅仅有一个线程可以拿到这个数据
- python之所以会产生这种不好的状况是因为python启用一个线程是调用操作系统原生线程,就是C接口
- 但是这仅仅是CPython这个版本的问题,在PyPy,中就没有这种缺陷
- 用户锁:线程锁(互斥锁Mutex) :当前线程还未操作完成前其他所有线程都无法对其操作,即使已经释放了GIL锁
- 在有GIL锁时为何还需要用户锁
- GIL锁只能保证同一时间只能有一个线程对某个资源操作,但当上一个线程还未执行完毕时可能就会释放GIL,其他线程就可以操作了
- 线程锁的原理
- 当一个线程对某个资源进行CPU计算的操作时加一个线程锁,只有当前线程计算完成主动释放锁,其他线程才能对其操作
- 这样就可以防止还未计算完成,释放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 修改后释放
在有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
- 使用线程锁解决上面问题的原理
- 在GIL锁中再加一个线程锁,线程锁是用户层面的锁
- 线程锁就是一个线程在对数据操作前加一把锁,防止其他线程复制或者操作这个数据
- 只有这个线程对数据操作完毕后才会释放这个锁,其他线程才能操作这个数据
- 定义一个线程锁非常简单只用三步
1 >> lock = threading.Lock() #定义一把锁 2 >> lock.acquire() #对数据操作前加锁防止数据被另一线程操作 3 >> lock.release() #对数据操作完成后释放锁
6、死锁
- 死锁定义:
- 两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象若无外力作用,它们都将无法推进去。
- 死锁举例:
- 启动5个线程,执行run方法,假如thread1首先抢到了A锁,此时thread1没有释放A锁,紧接着执行代码mutexB.acquire(),抢到了B锁,
- 在抢B锁时候,没有其他线程与thread1争抢,因为A锁没有释放,其他线程只能等待
- thread1执行完func1函数,然后执行func2函数,此时thread1拿到B锁,然后执行time.sleep(2),此时不会释放B锁
- 在thread1执行func2的同时thread2开始执行func1获取到了A锁,然后继续要获取B锁
- 不幸的是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() 解决死锁问题
- 递归锁的作用是同一线程中多次请求同一资源,但是不会参数死锁。
- 这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。
- 直到一个线程所有的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锁
8、Semaphore(信号量)
- 互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据
- 比如厕所有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()
进程
- 多线程和多进程各自应用场景
- I/O操作不占用CPU(从硬盘,网路读入数据等)
- 计算占用CPU,这种情况最好不用多线程
- python多线程不适合CPU密集型的任务,适合I/O密集型的任务
- python的多进程适合CPU密集型任务
- 一次性起多个进程,并在进程中调用线程
import multiprocessing,time,threading #3 被多线程调用的函数 def thread_run(): print(threading.get_ident()) #打印线程id号 time.sleep(2) #2 被多进程调用的函数,以及在这个函数中起一个进程 def run(name): time.sleep(2) print("hello",name) t = threading.Thread(target=thread_run,) #在进程调用的函数中启用一个线程 t.start() #1 一次性启动多个进程 if __name__ == '__main__': for i in range(10): p = multiprocessing.Process(target=run,args=('bob %s'%i,)) #启用一个多线程 p.start()
- 进程间互相访问数据的三种方法
- 注:不同进程间内存是不共享的,所以互相之间不能访问对方数据
- 在父进程中定义队列q,使用父进程启用一个子进程,子进程中无法操作父进程的q
from multiprocessing import Process import queue import threading def f(): q.put([42, None, 'hello']) if __name__ == '__main__': q = queue.Queue() #1 在父进程中定义一个队列实例q # p = threading.Thread(target=f,) #在线程程中就可以相互访问,线程中内存共享 p = Process(target=f,) #2 在父进程中起一个子进程 p,在子进程中使用父进程的q会报错 p.start() print(q.get()) p.join()
- 利用Queues实现父进程到子进程(或子进程间)的数据传递
-
- 我们以前学的queue是线程queue.Queue()只有在同一个进程的线程间才能访问
- 如果两个进程间想要通信必须要使用进程Queue,用法和多线程的相同
- queue.Queue()是线程q不可以传递给子进程,但是Queue是进程q,父进程会将进程q克隆了一份给子进程
- 既然是两个q为什么在子进程中在q中放入一个数据在父进程中可以取出来呢? 其实原因是这样的:
- 子进程向q中放入数据的时候,用pickle序列化将数据放到一个中间地方(翻译),翻译又把子进程放
- 入的数据用pickle反序列化给父进程,父进程就可以访问这个q了,这样就实现了进程间的数据通信了
- 在多线程中两个线程可以修改同一份数据,而Queue仅仅实现了进程间的数据传递
from multiprocessing import Process, Queue def f(qq): # 将符进程中的q传递过来叫qq qq.put([42, None, 'hello']) # 此时子进程就可以使用符进程中的q if __name__ == '__main__': q = Queue() # 使用Queue()在父进程中定义一个队列实例q p = Process(target=f, args=(q,)) # 在父进程中起一个子进程 p,将父进程刚定义的q传递给子进程p p.start() print(q.get()) p.join() # 运行结果: [42, None, 'hello']
- 使用管道pipe实现两个进程间数据传递
- 说明:其实pip实现进程间通信就好像一条电话线一样,一个在电话线这头发送,一个在电话线那头接收
from multiprocessing import Process, Pipe def f(conn): conn.send([42, None, 'hello']) # 3 子进程发送数据,就像socket一样 print("son process recv:", conn.recv()) conn.close() if __name__ == '__main__': parent_conn, child_conn = Pipe() # 1 生成一个管道实例,实例一生成就会生成两个返回对象,一个是管道这头,一个是管道那头 p = Process(target=f, args=(child_conn,)) # 2 启动一个子进程将管道其中一头传递给子进程 p.start() print(parent_conn.recv()) # 4 父进程收消息 # prints "[42, None, 'hello']" parent_conn.send('i am parent process') p.join() # 运行结果: # [42, None, 'hello'] # son process recv: i am parent process
- Managers实现很多进程间数据共享
-
- 说明:manager实质和Queue一样,启用是个线程其实就是将字典或者列表copy十份
from multiprocessing import Process, Manager import os def f(d, l): d[1] = '1' # 是个进程对字典放入的是同一个值,所以看上去效果不明显 l.append(os.getpid()) # 将这是个进程的进程id放入列表中 if __name__ == '__main__': with Manager() as manager: # 1 将Manager()赋值给manager d = manager.dict() # 2 定义一个可以在多个进程间可以共享的字典 l = manager.list(range(5)) # 3 定义一个可以在多个进程间可以共享的列表,默认写五个数据 p_list = [] for i in range(10): # 生成是个进程 p = Process(target=f, args=(d, l)) # 将刚刚生成的可共享字典和列表传递给子进程 p.start() p_list.append(p) for res in p_list: res.join() print(d) print(l)
- 进程之间需要锁的原因
- 说明:虽然每个进程是独立运行的,但是他们共享同一块屏幕,如果大家都在屏幕打数据就会打乱了
from multiprocessing import Process, Lock def f(l, i): l.acquire() #一个进程要打印数据时先锁定 print('hello world', i) l.release() #打印完毕后就释放这把锁 if __name__ == '__main__': lock = Lock() #先生成一把锁 for num in range(5): Process(target=f, args=(lock, num)).start() # 运行结果: # hello world 4 # hello world 0 # hello world 2 # hello world 3 # hello world 1
- 进程池
- 进程池的作用就是限制同一时间可以启动进程的=数量
- 进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进那么程序就会等待,直到进程池中有可用进程为止。
- 进程池中有两个方法:
- apply: 多个进程异步执行,一个一个的执行
- apply_async: 多个进程同步执行,同时执行多个进程
from multiprocessing import Process,Pool import time,os def foo(i): time.sleep(2) print("in the process",os.getpid()) #打印子进程的pid return i+100 def call(arg): print('-->exec done:',arg,os.getpid()) if __name__ == '__main__': pool = Pool(3) #进程池最多允许5个进程放入进程池 print("主进程pid:",os.getpid()) #打印父进程的pid for i in range(10): #用法1 callback作用是指定只有当Foo运行结束后就执行callback调用的函数,父进程调用的callback函数 pool.apply_async(func=foo, args=(i,),callback=call) #用法2 串行 启动进程不在用Process而是直接用pool.apply() # pool.apply(func=foo, args=(i,)) print('end') pool.close() #关闭pool pool.join() #进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。
僵尸进程
- 僵尸进程定义
- 僵尸进程产生的原因就是父进程产生子进程后,子进程先于父进程退出
- 但是父进程由于种种原因,并没有处理子进程发送的退出信号,那么这个子进程就会成为僵尸进程。
- 用python写一个僵尸进程
#!/usr/bin/env python #coding=utf8 import os, sys, time #产生子进程 pid = os.fork() if pid == 0: #子进程退出 sys.exit(0) #父进程休息30秒 time.sleep(30) # 先产生一个子进程,子进程退出,父进程休息30秒,那就会产生一个僵尸进程
[root@linux-node4 ~]# ps -ef| grep defunct root 110401 96083 0 19:11 pts/2 00:00:00 python defunct.py root 110402 110401 0 19:11 pts/2 00:00:00 [python] <defunct> root 110406 96105 0 19:11 pts/3 00:00:00 grep --color=auto defunct
协程(Coroutine)
1、什么是协程(进入上一次调用的状态)
- 协程,又称微线程,纤程,协程是一种用户态的轻量级线程。
- 线程的切换会保存到CPU的栈里,协程拥有自己的寄存器上下文和栈,
- 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈
- 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态
- 协程最主要的作用是在单线程的条件下实现并发的效果,但实际上还是串行的(像yield一样)
2、协程的好处
- 无需线程上下文切换的开销(可以理解为协程切换就是在不同函数间切换,不用像线程那样切换上下文CPU)
- 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突
- 用法:最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
3、协程缺点
- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上
- 线程阻塞(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()
5、协程为何能处理大并发1:Greenlet遇到I/O手动切换
- 协程之所以快是因为遇到I/O操作就切换(最后只有CPU运算)
- 这里先演示用greenlet实现手动的对各个协程之间切换
- 其实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这个协程
6、协程为何能处理大并发2:Gevent遇到I/O自动切换
- Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程
- 在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程
- Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
- 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), ]) # 运行结果: # 第一次打印 # 第二次打印 # 第三次打印 # 第四次打印 # 第五次打印 # 第六次打印
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()猴子补丁作用
-
- 用过gevent就会知道,会在最开头的地方gevent.monkey.patch_all();
- 作用是把标准库中的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)
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()
10、协程本质原理
- 协程1通过os去读一个file,这个时候就是一个io操作,在调用os的接口前,就会有一个列表
- 协程1的这个操作就会被注册到这个列表中,然后就切换到其他协程去处理;
- 等待os拿到要读file后,也会把这个文件句柄放在这个列表中
- 然后等待在切换到协程1的时候,协程1就可以直接从列表中拿到数据,这样就可以实现不阻塞了
- epoll返回给协程的任务列表在内核态,协程在用户态,用户态协程是不能直接访问内核态的任务列表的,所以需要拷贝整个内核态的任务列表到用户态,供协程去访问和查询
11、epoll处理 I/O 请求原理
- epoll() 中内核则维护一个链表,epoll_wait 直接检查链表是不是空就知道是否有文件描述符准备好了。
- 在内核实现中 epoll 是根据每个 sockfd 上面的与设备驱动程序建立起来的回调函数实现的。
- 某个 sockfd 上的事件发生时,与它对应的回调函数就会被调用,来把这个 sockfd 加入链表,其他处于“空闲的”状态的则不会。
- epoll上面链表中获取文件描述,这里使用内存映射(mmap)技术, 避免了复制大量文件描述符带来的开销
- 内存映射(mmap):内存映射文件,是由一个文件到一块内存的映射,将不必再对文件执行I/O操作
12、select处理协程
- 拷贝所有的文件描述符给协程,不论这些任务的是否就绪,都会被返回
- 那么协程就只能for循环去查找自己的文件描述符,也就是任务列表,select的兼容性非常好,支持linux和windows
13、select、epool、pool
- I/O的实质是什么?
- I/O的实质是将硬盘中的数据,或收到的数据实现从内核态 copy到 用户态的过程
- 本文讨论的背景是Linux环境下的network IO。
- 比如微信读取本地硬盘的过程
- 微信进程会发送一个读取硬盘的请求----》操作系统
- 只有内核才能够读取硬盘中的数据---》数据返回给微信程序(看上去就好像是微信直接读取)
- 用户态 & 内核态
- 系统空间分为两个部分,一部分是内核态,一部分是用户态的部分
- 内核态:内核态的空间资源只有操作系统能够访问
- 用户态:我们写的普通程序使用的空间
- select
- 只能处理1024个连接(每一个请求都可以理解为一个连接)
- 不能告诉用户程序,哪一个连接是活跃的
- pool
- 只是取消了最大1024个活跃的限制
- 不能告诉用户程序,哪一个连接是活跃的
- epool
- 不仅取消了1024这个最大连接限制
- 而且能告诉用户程序哪一个是活跃的
- select实现单线程下的多并发(必须是非阻塞模式)
import select import socket import queue server = socket.socket() server.bind(("localhost",9999)) server.listen(1000) server.setblocking(False) #设置非阻塞模式,recv没数据不阻塞,server.accept不阻塞但报错 msg_dic = {} #因为刚开没有连接可以监控,所以将server自己交给内核监测,只要server自己活动了就代表有人连我了 inputs = [server,] #有多少连接需要监测就必须放到inputs列表中,将列表交给select相当于交给内核 outputs = [] #第一个inputs是指定要内核监控那些链接,链接中只要有一个有数据就返回所有连接 #第二个outputs是存放还未发送的数据,下次就会发送 #第三个inputs也是监控所有连接,但是只有连接出问题是才返回所有连接 while True: readable,writeable,exceptional = select.select(inputs, outputs, inputs) print(readable,writeable,exceptional) for r in readable: if r is server: #代表来了一个新链接 conn,addr = server.accept() print("来了一个新链接:",addr) inputs.append(conn) #因为这个新建立的链接还没发数据过来,现在收就报错 #所以要想实现这个客户端发数据server端知道,就需要让这个select再监测这个 msg_dic[conn] = queue.Queue() #为每个链接都建立一个队列,里面存返回给客户端数据 else: #代表有客户端发数据过来 data = r.recv(1024) print("收到数据",data) msg_dic[r].put(data) outputs.append(r) #放入返回的连接队列里 # r.send(data) # print('send done') for w in writeable: data_to_client = msg_dic[w].get() w.send(data_to_client) #返回给客户端原数据 outputs.remove(w) #确保下次循环的时候writeable,不返回这个已经处理完的链接啦 for e in exceptional: #有断开的连接就从各个列表中删除 if e in outputs: outputs.remove(e) inputs.remove() del msg_dic[e]
import socket HOST = 'localhost' # The remote host PORT = 9999 # The same port as used by the server s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print("s",s) 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()
- 说明:server.setblocking(False) 设置成非阻塞模式作用
- python默认情况下所有的socket都是blocking,即阻塞然后等待I/O操作完成,接收数据
- 当使用协程实现单线程并发效果时需要设置成非租塞模式,不等待I/O操作
- 使用selector模块实现单线程下的多并发效果
- 作用:selector的实质是对select,poll,epoll的封装,他默认使用epoll,但是如果系统不支持就用select
import selectors,socket sel = selectors.DefaultSelector() #1 生成一个selector对象 def accept(sock, mask): #只要来一个新链接就调用accept conn, addr = sock.accept() # 创建这个链接 conn.setblocking(False) #6 把这个链接设置为非阻塞模式 sel.register(conn, selectors.EVENT_READ, read) #只有把活动连接注册到sel中sel才会去检测它 #7 把新建立的链接conn又放到selector注册对象sel里了,这时的回调函数变成read了 #这时如果再活动就会调用read了,执行完accept后就会返回到events = sel.select()继续监测 def read(conn, mask): #2第二次卡住: 客户端连接成功就卡在这里,等待客户端发送数据 try: #如果客户端断开后,收数据就会引发ConnectionResetError异常 data = conn.recv(1024) # Should be ready if data: conn.send(data) # Hope it won't block except ConnectionResetError as e: print('closing', conn) sel.unregister(conn) #取消注册,关闭链接 conn.close() sock = socket.socket() sock.bind(('localhost', 9999)) sock.listen(100) sock.setblocking(False) #2 设置为非阻塞模式 #3 将前面写的sock(server实例)注册到selselector对象中,让selector对象sel监测自己,自己活跃说明有链接或者发送数据 sel.register(sock, selectors.EVENT_READ, accept) while True: #第一次有活动可定有新链接了,只要有新链接就会调用accept方法建立链接 print("监测活跃:新链接或者已连接发送数据") events = sel.select() #4 第一次卡住: 运行服务端就会卡在这里等待客户端连接 # 这里虽然写的select但是可能是epoll看系统支持什么 # 当有连接过来时,就会将连接实例赋值给events for key, mask in events: # for循环这个events,默认是阻塞的,只要不阻塞肯定有新的连接 callback = key.data # 这里的key.data就是回调函数内存地址(accept或者read) callback(key.fileobj, mask) #5 key.fileobj是连接的socket实例conn和addr, mask=1 不知道什么 #callback(key.fileobj, mask)是执行实例的回调函数 #如果是新连接回调函数是accept函数,如果已连接发数据回调函数是read # 注:for循环中key包含以下内容 # SelectorKey( #fileobj是连接实例:conn,addr # fileobj = <socket.socket fd=320, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9998)>, # fd = 320, #文件描述符 # events = 1, # data = <function accept at 0x004D64B0>)) #回调函数内存地址
import socket HOST = 'localhost' # The remote host PORT = 9999 # The same port as used by the server s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print("s",s) 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()
# 使用selector的几个关键步骤 # 1)sel = selectors.DefaultSelector() # 生成一个selector对象 # 2)sock.setblocking(False) # 设置为非阻塞模式 # 3)sel.register(sock, selectors.EVENT_READ, accept) # # 将前面写的sock(server实例)注册到selselector对象中,让selector对象sel监测,accept是回调函数 # 4)events = sel.select() # 1第一次卡住: 运行服务端就会卡在这里等待客户端连接 # 5)callback(key.fileobj, mask) # key.fileobj是连接的socket实例conn, mask=1 不知道什么东东 # 6)sel.register(conn, selectors.EVENT_READ, read) # 将回调函数从accept变成read # 当客户端第一次连接时会使用accept作为回调函数,连接成功后就使用read变成回调函数 # 7)当执行完accept函数后就再次回到events = sel.select() # 第一次卡住的地方等待活跃数据 # 8)如果此时活跃的是已经连接的客户端,会调用callback(key.fileobj, mask),因为客户端第一次连接的时候调用 # 的是accept方法,执行了sel.register(conn, selectors.EVENT_READ, read),所以回调函数已经变成了read,所 # 以如果是客户端发送数据过来调用的是read方法,而不是accept方法 # 9)如果在第七步中活跃的是一个新连接,那么回调函数依然是accept,就会重复上面步骤建立一个新连接 # 注:无论是新连接还旧链接发送数据过来,实质上没有太多区别,都是使用callback(key.fileobj, mask)调用回调函数 # 1.但是如果是第一次连接回调函数是accept,在调用完accept后就将回调函数变成了read,执行完accept函 # 后会回到第一次卡住的地方events = sel.select(),监测活跃的连接 # 2.如果活跃的连接是已经连接的客户端发送数据,就会调用read函数去接收数据,运行完read后也会回到 # events = sel.select(),监测活跃的连接
Python进程池和线程池(ThreadPoolExecutor&ProcessPoolExecutor)
- 简介 参考官网
- Python标准库为我们提供了threading和multiprocessing模块编写相应的多线程/多进程代码
- 但是当项目达到一定的规模,频繁创建/销毁进程或者线程是非常消耗资源的,这个时候我们就要编写自己的线程池/进程池,以空间换时间。
- 但从Python3.2开始,标准库为我们提供了concurrent.futures模块,它提供了ThreadPoolExecutor和ProcessPoolExecutor两个类,
- 实现了对threading和multiprocessing的进一步抽象,对编写线程池/进程池提供了直接的支持。
- Executor和Future
1. Executor
-
- concurrent.futures模块的基础是Exectuor,Executor是一个抽象类,它不能被直接使用。
- 但是它提供的两个子类ThreadPoolExecutor和ProcessPoolExecutor却是非常有用
- 我们可以将相应的tasks直接放入线程池/进程池,不需要维护Queue来操心死锁的问题,线程池/进程池会自动帮我们调度。
2. Future
-
- Future你可以把它理解为一个在未来完成的操作,这是异步编程的基础,
- 传统编程模式下比如我们操作queue.get的时候,在等待返回结果之前会产生阻塞,cpu不能让出来做其他事情,
- 而Future的引入帮助我们在等待的这段时间可以完成其他的操作。
- ThreadPoolExecutor(线程池)
from concurrent.futures import ThreadPoolExecutor import time def return_future_result(message): time.sleep(2) return message pool = ThreadPoolExecutor(max_workers=2) # 创建一个最大可容纳2个task的线程池 future1 = pool.submit(return_future_result, ("hello")) # 往线程池里面加入一个task future2 = pool.submit(return_future_result, ("world")) # 往线程池里面加入一个task print(future1.done()) # 判断task1是否结束 time.sleep(3) print(future2.done()) # 判断task2是否结束 print(future1.result()) # 查看task1返回的结果 print(future2.result()) # 查看task2返回的结果 # 运行结果: # False # 这个False与下面的True会等待3秒 # True # 后面三个输出都是一起打出来的 # hello # world
import concurrent.futures import urllib.request URLS = ['http://httpbin.org', 'http://example.com/', 'https://api.github.com/'] def load_url(url, timeout): with urllib.request.urlopen(url, timeout=timeout) as conn: return conn.read() # We can use a with statement to ensure threads are cleaned up promptly with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: # Start the load operations and mark each future with its URL # future_to_url = {executor.submit(load_url, url, 60): url for url in URLS} # 这一句相当于下面for循环获取的字典 future_to_url = {} for url in URLS: future_to_url[executor.submit(load_url,url,60)] = url # {'future对象':'url'} future对象作为key,url作为value for future in concurrent.futures.as_completed(future_to_url): # as_completed返回已经有返回结果的future对象 url = future_to_url[future] # 通过future对象获取对应的url try: data = future.result() # 获取future对象的返回结果 except Exception as exc: print('%r generated an exception: %s' % (url, exc)) else: print('%r page is %d bytes' % (url, len(data)))
from concurrent.futures import ThreadPoolExecutor # 创建线程池 executor = ThreadPoolExecutor(10) def test_function(num1,num2): return "%s + %s = %s"%(num1,num2,num1+num2) result_iterators = executor.map(test_function,[1,2,3],[5,6,7]) for result in result_iterators: print(result) # 1 + 5 = 6 # 2 + 6 = 8 # 3 + 7 = 10
import concurrent.futures import urllib.request URLS = ['http://httpbin.org', 'http://example.com/', 'https://api.github.com/'] def load_url(url): with urllib.request.urlopen(url, timeout=60) as conn: return conn.read() # We can use a with statement to ensure threads are cleaned up promptly with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: future_dic = {} for url, data in zip(URLS, executor.map(load_url, URLS)): print('%r page is %d bytes' % (url, len(data))) future_dic[url] = data # {'url':'执行结果'} url作为key,执行结果作为value # 'http://httpbin.org' page is 13011 bytes # 'http://example.com/' page is 1270 bytes # 'https://api.github.com/' page is 2039 bytes
使用线程池、进程池、协程向多个url并发获取页面数据比较
- 特点:
-
- 进程:启用进程非常浪费资源
- 线程:线程多,并且在阻塞过程中无法执行其他任务
- 协程:gevent只用起一个线程,当请求发出去后gevent就不管,永远就只有一个线程工作,谁先回来先处理
- 使用for循环串行拿取页面数据(第四:性能最差)
import requests url_list = [ 'https://www.baidu.com', 'http://dig.chouti.com/', ] for url in url_list: result = requests.get(url) print(result.text)
- 进程池实现并发(第三)
- 缺点:启用进程非常浪费资源
import requests from concurrent.futures import ProcessPoolExecutor def fetch_request(url): result = requests.get(url) print(result.text) url_list = [ 'https://www.baidu.com', 'https://www.google.com/', #google页面会卡住,知道页面超时后这个进程才结束 'http://dig.chouti.com/', #chouti页面内容会直接返回,不会等待Google页面的返回 ] if __name__ == '__main__': pool = ProcessPoolExecutor(10) # 创建线程池 for url in url_list: pool.submit(fetch_request,url) # 去线程池中获取一个进程,进程去执行fetch_request方法 pool.shutdown(False)
- 线程池实现并发(第二)
- 缺点: 创建一个新线程将消耗大量的计算资源,并且在阻塞过程中无法执行其他任务。
- 例: 比如线程池中10个线程同时去10个url获取数据,当数据还没来时这些线程全部都在等待,不做事。
import requests from concurrent.futures import ThreadPoolExecutor def fetch_request(url): result = requests.get(url) print(result.text) url_list = [ 'https://www.baidu.com', 'https://www.google.com/', #google页面会卡住,知道页面超时后这个进程才结束 'http://dig.chouti.com/', #chouti页面内容会直接返回,不会等待Google页面的返回 ] pool = ThreadPoolExecutor(10) # 创建一个线程池,最多开10个线程 for url in url_list: pool.submit(fetch_request,url) # 去线程池中获取一个线程,线程去执行fetch_request方法 pool.shutdown(True) # 主线程自己关闭,让子线程自己拿任务执行
from concurrent.futures import ThreadPoolExecutor import requests def fetch_async(url): response = requests.get(url) return response.text def callback(future): print(future.result()) url_list = ['http://www.github.com', 'http://www.bing.com'] pool = ThreadPoolExecutor(5) for url in url_list: v = pool.submit(fetch_async, url) v.add_done_callback(callback) pool.shutdown(wait=True)
- 协程:微线程实现异步(第一:性能最好)
- 特点 :gevent只用起一个线程,当请求发出去后gevent就不管,永远就只有一个线程工作,谁先回来先处理
import gevent import requests from gevent import monkey monkey.patch_all() # 这些请求谁先回来就先处理谁 def fetch_async(method, url, req_kwargs): response = requests.request(method=method, url=url, **req_kwargs) print(response.url, response.content) # ##### 发送请求 ##### gevent.joinall([ gevent.spawn(fetch_async, method='get', url='https://www.python.org/', req_kwargs={}), gevent.spawn(fetch_async, method='get', url='https://www.google.com/', req_kwargs={}), gevent.spawn(fetch_async, method='get', url='https://github.com/', req_kwargs={}), ])