1.Python内存管理
1.Python如何内存管理
-
引用计数:当一个Python对象被引用时其引用计数增加1,当其不再被一个变量引用时则计数减1,当引用计数等于0时对象被删除。弱引用不会增加引用计数。
-
垃圾回收:python会检查引用计数为0的对象,清除其在内存占的空间;循环引用对象则用一个循环垃圾回收器来回收
-
内存池机制:在Python中,许多时候申请的内存都是小块的内存,这些小块内存在申请后,很快又会被释放,由于这些内存的申请并不是为了创建对象,所以并没有对象一级的内存池机制。这就意味着Python在运行期间会大量地执行malloc(分配内存)和free(释放内存)的操作,频繁地在用户态和核心态之间进行切换,这将严重影响Python的执行效率。为了加速Python的执行效率,Python引入了一个内存池机制,用于管理对小块内存的申请和释放。
a) Python提供了对内存的垃圾收集机制,但是它将不用的内存放到内存池而不是返回给操作系统。 b) Python中所有小于256个字节的对象都使用pymalloc实现的分配器,而大的对象则使用系统的 malloc。另外Python对象,如整数,浮点数和List,都有其独立的私有内存池,对象间不共享他们的内存池。也就是说如果你分配又释放了大量的整数,用于缓存这些整数的内存就不能再分配给浮点数。
2.调优手段
-
手动垃圾回收
-
调高垃圾回收阈值
-
避免循环引用
3.内存泄露是什么?如何避免?
-
内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费,通过Python扩展模块gc 来查看不能回收的对象的详细信息。
-
防止内存泄露:有
__del__()
函数的对象间的循环引用是导致内存泄露的主凶。不使用一个对象时使用: del object 来删除一个对象的引用计数就可以有效防止内存泄露问题。 -
判断是否内存泄露:通过 sys.getrefcount(obj) 来获取对象的引用计数,并根据返回值是否为0来判断.
4.哪些操作会导致Python内存溢出,怎么处理?
- 内存溢出原因:
1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据; 一般比如数据查询未做分页处理
2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
3.代码中存在死循环或循环产生过多重复的对象实体
4.使用的第三方软件中的BUG; 一般引用第三方jar包过多会出现此类问题
5.启动参数内存值设定的过小 这种可能性很小服务器参数设置一般会出现这类问题毕竟都是开发 - 内存溢出的解决方案:
第一步,修改JVM启动参数,直接堆内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
2.Python的垃圾回收机制
-
引用计数
PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少.引用计数为0时,该对象生命就结束了。
优点:
- 简单
- 实时性
缺点:
- 维护引用计数消耗资源
- 循环引用
-
标记清除机制
基本思路是先按需分配,等到没有空闲内存的时候从寄存器和程序栈上的引用出发,遍历以对象为节点、以引用为边构成的图,把所有可以访问到的对象打上标记,然后清扫一遍内存空间,把所有没标记的对象释放。
-
分代技术
分代回收的整体思想是:将系统中的所有内存块根据其存活时间划分为不同的集合,每个集合就成为一个“代”,垃圾收集频率随着“代”的存活时间的增大而减小,存活时间通常利用经过几次垃圾回收来度量。
Python默认定义了三代对象集合,索引数越大,对象存活时间越长。
举例: 当某些内存块M经过了3次垃圾收集的清洗之后还存活时,我们就将内存块M划到一个集合A中去,而新分配的内存都划分到集合B中去。当垃圾收集开始工作时,大多数情况都只对集合B进行垃圾回收,而对集合A进行垃圾回收要隔相当长一段时间后才进行,这就使得垃圾收集机制需要处理的内存少了,效率自然就提高了。在这个过程中,集合B中的某些内存块由于存活时间长而会被转移到集合A中,当然,集合A中实际上也存在一些垃圾,这些垃圾的回收会因为这种分代的机制而被延迟。
3.进程,线程,协程
进程里有线程,线程里有协程
1.进程(并行)
1.定义:
-
进程是系统进行资源分配和调度的一个独立单位(不共享全局变量),进程需要相应的系统资源:内存、时间片、pid,开销大
-
多进程适合在CPU密集操作(cpu操作指令比较多,如位多的的浮点运算)
-
进程是并行
并发和并行的区别?
- 并发:并发的实质是一个物理CPU(也可以多个物理CPU) 在若干道程序之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率,不会在同一时刻同时运行,存在交替执行的情况。
- 并行:指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行
2.组成
-
程序:我们编写的程序用来描述进程要完成哪些功能以及如何完成
-
数据集:数据集则是程序在执行过程中所需要使用的资源
-
进程控制块:进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志
- 阐释:进程与进程之间都占用的是独立的内存块,它们彼此之间的数据也是独立的
- 优点:同时利用多个CPU,能够同时进行多个操作
- 缺点:耗费资源(需要重新开辟内存空间)
3.multiprocess
模块介绍
1.管理进程模块:
- Process(用于创建进程模块)
- Pool(用于创建管理进程池)
1.进程Process
-
Process([group [, target [, name [, args [, kwargs]]]]])
- 需要使用关键字的方式来指定参数
- args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号
-
参数介绍:
-
group参数未使用,值始终为None
-
target表示调用对象,即子进程要执行的任务
-
args表示调用对象的参数元组,args=(1,2,'egon',)
-
kwargs表示调用对象的字典,kwargs={'name':'egon','age':18}
-
name为子进程的名称
-
-
方法介绍:
- p.start():启动进程,并调用该子进程中的p.run()
- p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法
- p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
- p.is_alive():如果p仍然运行,返回True
- p.join([timeout]):阻塞当前上下文环境的进程,直到调用此方法的进程终止或到达指定的timeout(可选参数),需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程
-
属性介绍:
-
p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程。设定为True后,p不能创建自己的新进程,必须在p.start()之前设置,主进程创建守护进程,守护进程会随着主进程的结束而结束。守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children
-
p.name:进程的名称
-
p.pid:进程的pid
-
p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)
-
p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性(了解即可)
-
在Windows操作系统中由于没有fork(linux操作系统中创建进程的机制),因此用process()直接创建子进程会无限递归创建子。所以必须把创建子进程的部分使用if name ==‘main’ 判断保护起来,import 的时候 ,就不会递归运行了
-
-
多进程开发中join和deamon的区别
-
join:当子线程调用join时,主线程会被阻塞,当子线程结束后,主线程才能继续执行。
-
deamon:当子进程被设置为守护进程时,主进程结束,不管子进程是否执行完毕,都会随着主进程的结束而结束。
-
-
Process模块创建并开启子进程的两种方式:
-
通过调用process模块的方式来创建进程
from multiprocessing import Process import time def start(name): time.sleep(1) print('hello', name) if __name__ == '__main__': p = Process(target=start, args=('zhangsan',)) p1 = Process(target=start, args=('lisi',)) p.start() p1.start() p.join()#hello zhangsan hello lisi
-
通过继承process类,重写__init__方法和run方法的方式。如果需要传参,必须写入到__init__方法里面,且必须加上super().init();因为父类Process里面也有__init__方法。
from multiprocessing import Process import time class MyProcess(Process): def __init__(self,name): super().__init__() self.name = name def run(name): time.sleep(1) print('hello', name) if __name__ == '__main__': p = MyProcess('zhangsan') p1 = MyProcess('lisi',) p.start() p1.start() p.join() #hello <MyProcess name='zhangsan' parent=25347 started> # hello <MyProcess name='lisi' parent=25347 started>
-
2.进程池Pool
-
创建进程池:
Pool([numprocess [,initializer [, initargs[maxtasksperchild,]]]]):
- 进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。我们可以用Pool类创建一个进程池, 展开提交的任务给进程池.
-
参数介绍:
- numprocess:要创建的进程数,如果省略,将默认为cpu_count()的值,可os.cpu_count()查看
- initializer:是每个工作进程启动时要执行的可调用对象,默认为None
- initargs:是要传给initializer的参数组
- maxtasksperchild:工作进程退出之前可以完成的任务数,完成后用一个新的工作进程来替代原进程,来让闲置的资源被释放。maxtasksperchild默认是None,意味着只要Pool存在工作进程就会一直存活。
-
进程池的方法:
-
Pool.apply(func[, args[, kwds]])
(同步进程池,阻塞,串行):使用arg和kwds参数调用func函数,结果返回前会一直阻塞,由于这个原因,apply_async()更适合并发执行,另外,func函数仅被pool中的一个进程运行。from multiprocessing import Pool import os,time def task(n): print('[%s] is running'%os.getpid()) time.sleep(2) print('[%s] is done'%os.getpid()) return n**2 if __name__ == '__main__': p = Pool(4) #创建4个进程 for i in range(1,3):#开2个任务 res = p.apply(task,args=(i,)) #同步的,等着一个运行完才执行另一个 print('本次任务的结束:%s'%res) p.close()#禁止往进程池内再添加任务 p.join() #在等进程池 print('主') ''' [26252] is running [26252] is done 本次任务的结束:1 [26251] is running [26251] is done 本次任务的结束:4 主'''
-
Pool.apply_async(func[, args[, kwds[, callback[, error_callback]]]])
(异步进程池,非阻塞,并行) : 是apply()的一个变体,会返回一个结果对象。如果callback被指定,那么callback可以接收一个参数然后被调用,当结果准备好回调时会调用callback,调用失败时,则用error_callback替换callback。 Callbacks应被立即完成,否则处理结果的线程会被阻塞。from multiprocessing import Process,Pool import time def Foo(i): time.sleep(1) return i+100 def Bar(arg): print('number::',arg) if __name__ == "__main__": pool = Pool(3)#定义一个进程池,里面有3个进程 for i in range(4): #callback是回调函数,就是在执行完Foo方法后会自动执行Bar函数,并且自动把Foo函数的返回值作为参数传入Bar函数 pool.apply_async(func=Foo, args=(i,),callback=Bar) pool.close()#关闭进程池 #time.sleep(2) # 达到3进程数后等待2秒再继续执行 #pool.terminate() # 达到3进程数立刻关闭线程池 pool.join()#进程池中进程执行完毕后再关闭,(必须先close在join) ''' number:: 100 number:: 101 number:: 102 number:: 103 '''
-
close() : 阻止更多的任务提交到pool,待任务完成后,工作进程会退出。
-
terminate() : 不管任务是否完成,立即停止工作进程。在对pool对象进程垃圾回收的时候,会立即调用terminate()。
-
join() : 主进程等待所有子进程执行完毕,在调用join()前,必须调用close() or terminate()。这样是因为被终止的进程需要被父进程调用wait(join等价与wait),否则进程会成为僵尸进程。
-
2.进程之间通信,资源共享
-
Queue,pipe
-
会阻塞进程,多线程安全
-
queue.Queue() #先进先出 queue.LifoQueue() #后进先出 queue.PriorityQueue() #优先级队列 queue.deque() #双线队列
-
队列都是在内存中操作,进程退出,队列清空
-
#队列 import time from multiprocessing import Process, Queue def start(q): q.put( 'hello') if __name__ == '__main__': q = Queue() p = Process(target=start, args=(q,)) p.start() print(q.get()) time.sleep(2) p.join() ''' 打印hello,等待2s后主进程才结束 '''
-
-
Value,Array
-
用Value或Array存储在一个共享内存地图里:如Value(‘d’,0.0),Array(‘i’,range(11)),其中“d”表示一个双精度的浮点数,“i”表示一个有符号的整数,再把他们作为位置参数传入args里。
from multiprocessing import Process, Value, Array def f(n, a): n.value = 3.1415927 for i in range(len(a)): a[i] = -a[i] if __name__ == '__main__': num = Value('d', 0.0) arr = Array('i', range(10)) p = Process(target=f, args=(num, arr)) p.start() p.join() print(num.value) print(arr[:]) # 输出: #3.1415927 #[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
-
-
Manager
-
不会堵塞进程, 而且是多进程安全的
-
通过with Manager() as man:返回的manager提供.list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array类型的支持
from multiprocessing import Process, Manager def f(d, l): d[1] = '1' d['2'] = 2 d[0.25] = None l.reverse() if __name__ == '__main__': with Manager() as manager: d = manager.dict() l = manager.list(range(10)) p = Process(target=f, args=(d, l)) p.start() p.join() print(d) print(l) # 输出结果: #{{1: '1', '2': 2, 0.25: None} #[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
-
3.进程同步(锁):
- 锁是为了确保数据一致性,比如读写锁,如果在一个进程读取但还没有写入的时候,另外的进程也同时读取了,并写入该值,则最后写入的值是错误的,这时候就需要锁。
- Lock:当多个进程使用同一份数据资源的时候,就会引发数据安全或顺序混乱问题,可以用lock.acquire(),lock.release()将资源锁起来,虽然使用加锁的形式实现了顺序的执行,但是程序又重新变成串行了,加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行修改,速度是慢了,但牺牲了速度却保证了数据的安全性。
- Queue和Pipe:效率高(多个进程共享一块内存的数据
- Semaphore: 和 Lock 稍有不同,Semaphore 相当于 N 把锁,获取其中一把就可以执行了。 信号量的总数 N 在构造时传入,s = Semaphore(N)。 和 Lock 一样,如果信号量为0,则进程堵塞,直到信号大于0
- Condition(条件对象)
- Event(事件)
- Lock(锁)
- RLock(可重入锁)
- Semaphore(信号量)
4.孤儿进程,僵尸进程,守护进程
-
孤儿进程:父进程退出,子进程还在运行,这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害
-
僵尸进程:一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。所以如果一个子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。
- 僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)。系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。
-
守护进程:是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等
- 守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。
- 一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
- 守护进程的名称通常以d结尾,比如sshd、xinetd、crond等
2.线程(并发)
1.定义
-
cpu调度执行的最小单位,也叫执行路径,不能独立存在,依赖进程存在,一个进程至少有一个线程,叫主线程,而多个线程共享内存(数据共享,共享全局变量),从而极大地提高了程序的运行效率,开销小。
-
同一进程内的线程共享该进程的数据
-
在主进程下开启多个线程,每个线程都跟主进程的pid一样,开启多个子进程,每个进程有不同的pid
-
多线程适合在IO密性型操作(读写数据操作比多的的,比如爬虫)
-
线程是并行(同一时刻多个任务同时在运行)
2.组成
-
它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
-
阐释:线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。线程可以共享(调用)进程的数据资源
-
优点:共享内存,IO操作时候,创造并发操作
3.线程模块threading
- 方法介绍
- t.start() : 激活线程,
- t.getName() : 获取线程的名称
- t.setName() : 设置线程的名称
- t.name : 获取或设置线程的名称
- t.is_alive() : 判断线程是否为激活状态
- t.isAlive() :判断线程是否为激活状态
- t.setDaemon() 设置为后台线程或前台线程(默认:False);通过一个布尔值设置线程是否为守护线程,必须在执行start()方法之后才可以使用。如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止;如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止
- t.isDaemon() : 判断是否为守护线程
- t.ident :获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None。
- t.join() :逐个执行每个线程,执行完毕后继续往下执行,该方法使得多线程变得无意义
- t.run() :线程被cpu调度后自动执行线程对象的run方法threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程
- threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
4.线程同步(锁),信号量,事件,队列,条件变量
-
同步锁:
Lock
(互斥锁)和RLock
(可重入锁)两个对象都有 acquire 方法和 release 方法。对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire() 和 release() 方法之间。RLock允许在同一线程中被多次acquire。而Lock却不允许这种情况。 如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的琐 -
死锁和递归锁:在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。解决死锁可以用递归锁
RLock
-
信号量(Semaphore):从意义上来讲,也可以称之为一种锁:信号量:指同时开几个线程并发。信号量用来控制线程并发数的,BoundedSemaphore或Semaphore管理一个内置的计数 器,每当调用acquire()时-1,调用release()时+1。计数器不能小于0,当计数器为 0时,Semaphore.acquire()将阻塞线程至同步锁定状态,直到其他线程调用Semaphore.release()。(类似于停车位的概念)BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数器的值是否超过了计数器的初始值,如果超过了将抛出一个异常
-
事件threading.Event
-
用于主线程控制其他线程的执行,一个线程设置Event对象,另一个线程等待Event对象,事件主要提供了三个方法 set、wait、clear
-
事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。
-
clear:将“Flag”设置为False
-
set:将“Flag”设置为True
-
Event.isSet() :判断标识位是否为Ture。
import threading def do(event): print('start') event.wait() print('execute') event_obj = threading.Event() for i in range(3): t = threading.Thread(target=do, args=(event_obj,)) t.start() event_obj.clear() inp = input('input:') if inp == 'true': event_obj.set() ''' start start start input:true execute execute execute '''
-
-
队列threding.Queue
-
因为列表是不安全的数据结构,所以引申了新的模块——队列,queue 模块中提供了同步的、线程安全的队列类,包括
queue.Queue() #先进先出 queue.LifoQueue() #后进先出 queue.PriorityQueue() #优先级队列 queue.deque() #双线队列
-
queue 模块中的常用方法:
-
queue.qsize() 返回队列的大小
-
queue.empty() 如果队列为空,返回True,反之False
-
queue.full() 如果队列满了,返回True,反之False
-
queue.full 与 maxsize 大小对应
-
queue.get([block[, timeout]])获取队列,timeout等待时间
-
queue.get_nowait() 相当queue.get(False)
-
queue.put([block[, timeout]) 写入队列,timeout等待时间,可选参数block默认为True,表示获取值的时候,如果队列为空,则阻塞,为False时,不阻塞
-
queue.put_nowait(item) 相当Queue.put(item, False)
-
queue.task_done() 在完成一项工作之后,queue.task_done()函数向任务已经完成的队列发送一个信号
-
queue.join() 实际上意味着等到队列为空,再执行别的操作
-
-
条件变量threading.Condition
- Condition类实现了一个conditon变量。可以像锁机制那样用,所以也有acquire ,release方法。conditiaon变量允许一个或多个线程等待,直到他们被另一个线程通知。Condition内部有一把锁,默认是Rlock,在调用wait()和notify() 之前必须先调用acquire()来获取这个锁,当wait()和notify()执行完后,需调用release()释放这个锁,在执行with condition时,会先执行acquire(),with结束了,执行release()。
- Condition有两层锁,最底层锁在调用wait()时会释放,同时会加一把锁到等待队列,等待notify()唤醒释放锁
- wait(timeout=None) : 等待通知,或者等到设定的超时时间。当调用这wait()方法时,如果调用它的线程没有得到锁,那么会抛出一个RuntimeError 异常。 wati()释放锁以后,在被调用相同条件的另一个进程用notify() or notify_all() 叫醒之前 会一直阻塞。wait() 还可以指定一个超时时间。
- 如果有等待的线程,notify()方法会唤醒一个在等待conditon变量的线程。notify_all() 则会唤醒所有在等待conditon变量的线程。 notify()和notify_all()不会释放锁,也就是说,线程被唤醒后不会立刻返回他们的wait() 调用。除非线程调用notify()和notify_all()之后放弃了锁的所有权。例子: 生产者-消费者模型
import threading import time def consumer(cond): with cond: print("consumer before wait") cond.wait() print("consumer after wait") def producer(cond): with cond: print("producer before notifyAll") cond.notifyAll() print("producer after notifyAll") condition = threading.Condition() c1 = threading.Thread(name="c1", target=consumer, args=(condition,)) c2 = threading.Thread(name="c2", target=consumer, args=(condition,)) p = threading.Thread(name="p", target=producer, args=(condition,)) c1.start() time.sleep(2) c2.start() time.sleep(2) p.start() ''' consumer before wait consumer before wait producer before notifyAll producer after notifyAll consumer after wait consumer after wait '''
5.什么是线程同步和互斥
- 同步:按预定的先后次序进行运行
- 互斥:当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步
6.生产者-消费者模型
-
产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。在生产者与消费者之间在加个缓冲区,形象的称之为仓库,生产者负责往仓库了进商 品,而消费者负责从仓库里拿商品,这就构成了生产者消费者模型。
-
生产者消费者模型的优点:
-
- 解耦 :假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化, 可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
- 支持并发:由于生产者与消费者是两个独立的并发体,他们之间是用缓冲区作为桥梁连接,生产者只需要往缓冲区里丢数据,就可以继续生产下一个数据,而消费者只需要从缓冲区了拿数据即可,这样就不会因为彼此的处理速度而发生阻塞
- 支持忙闲不均:当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。 等生产者的制造速度慢下来,消费者再慢慢处理掉。
import time,random import queue,threading q = queue.Queue() def Producer(name): count =1 while True: time.sleep(random.randrange(3)) if q.qsize()<3: # 只要盘子里小于3个包子,厨师就开始做包子 q.put(count) print("Producer %s has produced %s baozi.." %(name,count)) count += 1 def Consumer(name): count =1 while True: time.sleep(random.randrange(4)) if not q.empty(): # 只要盘子里有包子,顾客就要吃。 data = q.get() print(data) print('Consumer %s has eat %s baozi...' % (name,data)) else: # 盘子里没有包子 print("---no baozi anymore----") count+=1 p1 = threading.Thread(target=Producer,args=('A',)) c1 = threading.Thread(target=Consumer,args=('B',)) c2 = threading.Thread(target=Consumer,args=('C',)) p1.start() c1.start() c2.start()
-
3.协程:
1.定义
-
又称微线程,线程是系统级别的它们由操作系统调度,而协程则是程序级别的由程序根据需要自己调度。在一个线程中会有很多函数,我们把这些函数称为子程序,在子程序执行过程中可以中断去执行别的子程序,而别的子程序也可以中断回来继续执行之前的子程序,这个过程就称为协程。也就是说在同一线程内一段代码在执行过程中会中断然后跳转执行别的代码,接着在之前中断的地方继续开始执行,类似与yield操作。协程是一中多任务实现方式,它不需要多个进程或线程就可以实现多任务。
-
优点:①协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显②不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多③高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理
-
缺点:①无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用②进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
2.实现协程的方法
- 通过yield实现协程
- 通过from greenlet import greenlet实现协程:g1 = greenlet(A) #创建协程g1,g1.switch() #跳转至协程g1
- 通过import gevent实现协程:gevent会主动识别程序内部的IO操作,当子程序遇到IO后,切换到别的子程序。如果所有的子程序都进入IO,则阻塞.
- g1 = gevent.spawn(A) # 创建一个协程,gevent.sleep(1) #用来模拟一个耗时操作,注意不是time模块中的sleep,#每当碰到耗时操作,会自动跳转至其他协程,g1.join() #等待协程执行结束.
4.死锁
1.什么是死锁?
- 所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。
2.产生死锁的原因?
a. 竞争资源
- 系统中的资源可以分为两类:
- 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源;
- 不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
- 产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
- 产生死锁中的竞争资源另外一种指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁
b. 进程间推进顺序非法
- 若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁
- 例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁
3.死锁产生的4个必要条件?
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。
4.解决死锁的基本方法
预防死锁:
- 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
- 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
- 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
- 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
-
以确定的顺序获得锁
开发者可以尝试按照锁对象的hashCode值大小的顺序,分别获得两个锁,这样锁总是会以特定的顺序获得锁,那么死锁也不会发生。问题变得更加复杂一些,如果此时有多个线程,都在竞争不同的锁,简单按照锁对象的hashCode进行排序(单纯按照hashCode顺序排序会出现“环路等待”),可能就无法满足要求了,这个时候开发者可以使用银行家算法,所有的锁都按照特定的顺序获取,同样可以防止死锁的发生
-
超时放弃
当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。
避免死锁:
- 预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
- 银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。
检测死锁
- 首先为每个进程和每个资源指定一个唯一的号码;
- 然后建立资源分配表和进程等待表。
解除死锁:
当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:
- 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
- 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
#冒泡排序
def bubbleSort(arr):
n = len(arr)
# 遍历所有数组元素
for i in range(n):
# Last i elements are already in place
for j in range(0, n-i-1):
if arr[j] > arr[j+1] :
arr[j], arr[j+1] = arr[j+1], arr[j]
arr = [64, 34, 25, 12, 22, 11, 90]
bubbleSort(arr)
print ("排序后的数组:")
for i in range(len(arr)):
print ("%d" %arr[i]),