同步&异步
同步:提交完任务后就在原地等待,直到任务运行完毕后拿到任务的返回值,再继续运行下一行代码
异步:提交完任务后不在原地等待,直接运行下一行代码,不关心任务的执行过程,任务的返回值不管~~
异步效率高于同步 但是并不是所有任务都可以异步执行,判断一个任务是否可以异步的条件是,任务发起方是否立即需要执行结果
同步不等于阻塞 异步不等于非阻塞 当使用异步方式发起任务时 任务中可能包含io操作 异步也可能阻塞 同步提交任务 也会卡主程序 但是不等同阻塞,因为任务中可能在做一对计算任务,CPU没走
pool.submit(task,i)===>产生对象 j pool.shutdown(wait=True) # shutdown 关闭进程池入口,wait=True,等异步任务都运行完在运行下一行代码
lock、GIL 就是为了达到同步,来保证安全.
而异步在多进程/线程IO的时候能提高效率.
GIL全局解释器锁
即,有了GIL的存在,同一进程内的多个线程同一时刻只能有一个在运行,(意味着在Cpython中一个进程下的多个线程无法实现并行===》无法利用多核优势)避免多个线程同时对资源进行读写造成混乱
但不影响并发的实现
GIL可被比喻成执行权限,同一进程下所有线程要想执行都要先抢执行权限
Cpython解释器自带的垃圾回收管理机制不是线程安全的(不能保证多线程同时操作产生的问题)------解决了安全问题 但是降低了效率( 另,虽然有解决方案 但是由于牵涉太多,一旦修改则 很多以前的基于GIL的程序都需要修改,所以变成了历史遗留问题 )
GIL带来的问题
即使在多核处理器下 也无法无法真正的并行。
so,GIL仅存在Cpython中,其他解释器无此问题,因Cpython优势在于有大量C的库可调用,-----其仍为主流
垃圾回收管理机制
python的垃圾回收管理机制 用的是引用计数
GIL加锁与解锁时机
加锁:调用解释器时立即加锁
解锁:1.当前程序遇到IO时
2.时间过长
3.有一个优先级更高的程序替代了它
程序处理关键---IO,非计算
IO密集型:需要大量IO时间
计算密集型:全是计算任务
总结
单核下无论是IO密集型还是计算密集型GIL都不会产生任何影响
多核下,对于IO密集型任务,GIL会有细微的影响
Cpython中IO密集任务应采用多线程,计算密集型应采用多进程---Cpython中线程无法并行执行
死锁与递归锁(可重入锁)
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都
正在使用,所有这两个线程在无外力作用下将一直等待下去。更直观的死锁比如,有两个lock对象,同一资源分别被两个进程的lock.requare阻塞,就造成程序永久的阻塞
解决死锁就可以用递归锁
为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:
threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
GIL与自定义互斥锁
GIL使用用于保护解释器相关的数据,解释器也是一段程序,肯定有其定义各种数据 GIL并不能保证你自己定义的数据的安全,所以一旦你的程序中出现了多线程共享数据时就需要自己加锁
自定义互斥锁---------保护程序员自己的程序的资源安全(多线程时,保证修改共享数据时有序的修改,不会产生数据修改混乱) GIL ----------保护解释器资源的安全(保证同一时刻只有一个线程能使用到cpu)
GIL会在任务无法执行(无执行权限)时被强制释放,互斥锁即便无法执行也不会自动释放
线程拿到公共数据--申请GIL---调用解释器中程序--到操作系统---CPU执行--执行时间到,释放GIL---再抢
当我们使用多线程的时候,每一个进程中只有一个GIL,那么这多个线程中谁拿到GIL,谁就可以使用cpu(ps:多个进程有多个Gil,但每个进程中只有一个GIL),所以当python用cpython作为解释器的时候,多线程就不是真正意义上的多线程,属于伪并发的多线程。
线程先得到cpu使用权 GIL锁使得线程可以使用解释器,得到lock 使得线程可以执行
- 首先假设只有一个进程,这个进程中有两个线程 Thread1,Thread2, 要修改共享的数据date, 并且有互斥锁: 执行以下步骤: 多线程运行,假设Thread1获得GIL可以使用cpu,这时Thread1获得 互斥锁lock,Thread1可以改date数据(但并没有开始修改数据); Thread1线程在修改date数据前发生了 i/o操作 或者 ticks计数满100((注意就是没有运行到修改data数据),这个时候 Thread1 让出了Gil,Gil锁可以被竞争); Thread1 和 Thread2 开始竞争Gil (注意:如果Thread1是因为i/o 阻塞 让出的Gil,Thread2必定拿到Gil,如果Thread1是因为ticks计数满100让出Gil这个时候Thread1 和 Thread2 公平竞争); 假设 Thread2正好获得了GIL, 运行代码去修改共享数据date,由于Thread1有互斥锁lock,所以Thread2无法更改共享数据date,这时Thread2让出Gil锁, GIL锁再次发生竞争; 假设Thread1又抢到GIL,由于其有互斥锁Lock所以其可以继续修改共享数据data,当Thread1修改完数据释放互斥锁lock,Thread2在获得GIL与lock后才可对data进行修改
并发的套接字通讯
练习:
执行客户端程序,用户可选的功能有: 1、登录 2、注册 3、上传 4、下载
思路解析: 1、执行登录,输入用户名cgon,密码123,对用户名egon和密码进行hash校验,并加盐处理,将密文密码发送到服务端, 与服务端事先存好用户名与密文密码进行对比,对比成功后, 在服务端内存中用hash算法生成一个随机字符串比如eadc05b6c5dda1f8772c4f4ca64db110 然后将该字符串发送给用户以及登录成功的提示信息发送给客户端,然后在服务端存放好 current_users={ 'a3sc05b6c5dda1f8313c4f4ca64db110':{'uid':0,'username':'alex'}, 'e31adfc05b6c5dda1f8772c4f4ca64b0':{'uid':1,'username':'lxx'}, 'eadc05b6c5dda1f8772c4f4ca64db110':{'uid':2,'username':'cgon'}, } 这个不太合适吧 反过来存? 用户在收到服务端发来的'eadc05b6c5dda1f8772c4f4ca64db110'以及登录成功的提示信息后,以后的任何操作都会携带该随 机字符串'eadc05b6c5dda1f8772c4f4ca64db110‘,服务端会根据该字符串获取用户信息来进行与该用户匹配的操作 在用户关闭连接后,服务端会从current_users字典中清除用户信息,下次重新登录,会产生新的随机字符串 这样做的好处: 1、用户的敏感信息全都存放到服务端,更加安全 2、每次登录都拿到一个新的随机的字符串,不容易被伪造 2、执行注册功能,提交到服务端,然后存放到文件中,如果用户已经存在则提示用户已经注册过,要求重新输入用户信息 3、执行上次下载功能时会携带用户的随机字符串到服务端,如果服务端发现该字符串not in current_users,则要求用户先登录
池 :帮助创建、管理线程(进程)
参数 最大进程数,不设置就默认造与CPU核数等量的进程数
最大线程数,不设置就默认造与CPU核数*5
池子大小固定,任务数固定,即 ,池的功能是限制启动的进程/线程数。当并发的任务数远超计算机承受能力时--无法一次性开启过多进程/线程数时,就应用池的概念将开启的进程/线程数限制在计算机可承受范围内
与信号量的区别 ,信号量也是一种锁 适用于保证同一时间能有多少个进程或线程访问
而线程/进程池,没有对数据访问进行限制仅仅是控制数量
线程池 from concurrent.futures import ThreadPoolExecutor pool = ThreadPoolExecutor() 进程池 from concurrent.futures import ProcessPoolExecutor pool = ProcessPoolExecutor() pool.submit()
生产者消费者模型
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
from multiprocessing import Process,Queue import time,random,os def consumer(q): while True: res=q.get() time.sleep(random.randint(1,3)) print('