信号量用处很大,但是常用的就是event和lock,信号量在有些地方必须使用,信号量跟锁有些像,锁lock(用了一个再要就不行了,所获取不到了,因为用了一次就需要release一次,没有release,就获取不到,要么等会,要么就一直等,)
信号量内部会维护一个倒计数器,每一次获取这个锁的时候,获取一次,往下减1,当acquire方法,再次调用的时候,会发现计数为0,就阻塞请求的线程
当有线程还回来,刚才请求(阻塞的)就可以获得一次了,计数大于0,恢复阻塞的线程
semaphore构造方法默认一个,代表acquire一下就没了,value小于0直接抛出异常
计数器等于0的时候,其他线程访问,如果acquire,阻塞的就是你被阻塞的时间,none就是永久阻塞
倒计数从3开始
condition和value,下面有acquire,说明有上下文可以用
这个倒计数是,每请求,往下减
再请求,就阻塞了,timeout=3,阻塞三秒超时
只有RLock才跟线程相关,其他是,所有线程共用一个对象,lock,event也是如此
现在用完了,就可以来归还了
在另一个线程内获取了这把锁
不管跨不跨线程,同一个信号量对象,只要问他获取一次,只要获取成功,值就会往下-1,直到为0为止,减到0还有线程,不管在哪个线程,只要对同一个信号对象,再调用acquire,就会阻塞,直到有一个线程,release,才算恢复,恢复了,才能去尝试获取信号量,获取到了,就可以执行,没获取到就继续等
会有一个超界的现象
如果是6,下面可以获取多少次,本来是限定使用三下,结果,通过多次release,相当于想要多少次就多少次,但是这样就破坏了初始化过程,初始化就是限制次数,一多release,这个次数就控制不住了
这样就只需做一件事情,boundesemaphore,有边界的semaphore
表示给太多了,不允许
手动release,忘了次数,就可以用with语法,但是with不可以跨函数,就要考虑如何去release,release多了就要考虑是否用有界的倒计数boundesemaphore来处理
这一句去掉就没有初始给的值了
有界的就是加了初始值是多少,比较当你归还的数字超过了初始给的,告诉你就是多还了,所以是多加了个属性来判断的,一个condition,一个value
当出现多还的情况,就用有界的semaphore来解决问题,如果多还,会抛出异常
信号量如何做,
应用,资源是有限的,要去打开链接,每一次建立链接的成本很高(一般不推荐平凡断开链接,和重新链接)
可以给一个链接池,这就是个存放链接的容器,最简单的就是列表。
这样就让大家找一找有没有空闲的列表,但是因为资源和带宽有限,不可能无限的创建(1000个请求,创建1000个链接,可以一个用完,不断开,让别人来使用)
有 三座桥,别人用的时候,其他就无法使用,最多同时有三个人来链接桥,第四个人只能等待,剩下来的只能等待,其中有座桥release,空闲了,等待中的某一个人就坐上桥了,
用一个减一个,当为0的,再来访问就不行了
资源有限,(讲锁的时候,lock,资源唯一),现在资源有三个,资源是有限个,当资源被用完之后,就不能再去开辟资源 了,资源有限不可能无限制服务,
所以当只有一个资源就用lock,三个资源就需要信号量
信号量跟锁类似,都是解决资源有限的问题,锁一般是唯一资源,上锁不能使用
模拟连接池,这个代码是有瑕疵的,只是实验
要抽象类,给链接类,和连接池类,池是容器类,里面方链接,在这个池对象内,放链接
在池内,有大小,是有限的,所以写代码的时候要处处节约资源,应该尽可能段的占用资源,用完就释放掉,但是链接资源有特殊的地方,只要是建立网络链接的时候,太耗时了,还不如创建好,大家一起利用,剩余的就得等,用完就空闲,让别人用
为什么用池就是因为需要平凡创建这个资源,每一次创建,消耗的内存和时间是非常大的,创建好之后就不要销毁了,反复利用,但不是共享利用,反复利用是独占的,你占了,我就不能用了
连接池和线程池是比较常见的,线程虽然是轻量,创建也是有代价的,就用多线程,重复利用线程
创建10个链接,把10个链接对象,放在self.pool的属性上,让别人使用
这是一个列表
有了池就不创建链接了,
这句话有大问题,多线程的时候不能保证这两句是一起执行的,一个线程正准备拿,就被切换了让别的线程拿走了,原本的线程就拿不到了
有可能多个线程都在调用这个函数,都想从线程池里拿出来,自己用,应该加lock
这一块其实用信号量比较好,不用lock,安全用有界的信号量
假设就count3个资源,3个被拿走了,如果第4个来拿,就永久阻塞了
release还回来
返回的的conn什么类型就需要判断,比如弄ID验证,还的对象必须有ID,类型对不对,ID对不对
应该是还完之后,才把信号量归还上去
抢的时候,先抢信号量,还的时候,先还资源,后归还信号量,
如果使用的时候,资源是单一的,又想独占,这时候建议使用lock,如果资源是有限个,只要没人归还,就不能用,就使用信号量
跨函数不能用with
get_conn,是有线程安全的,如果在多线程用,记得,几乎所有的语句都是可以打断的(甚至+=1都可以打断)
这句话太不安全
可以加锁,或者使用信号量
递减的方式使用信号量,
要注意谁前谁后
下面是得到链接来模拟使用时间,每个线程拿到链接,使用的秒数是不一致的,用完之后就归还,
如果池中有资源,请求者获取资源时信号量减1,拿走资源。当请求超过资源数,请求者只能等待。但使用者用完归还资源后信号量加1,等待线程就可以被唤醒拿走资源。
return_conn方法可以单独执行,有可能多归还链接,也就是会多release,所以要有界信号量BoundedSemphore
多线程,多release就会出现问题
同一个链接归还多次,也是要判断的,在生产环境就要判断别人还的是什么
正常情况下都会先获取信号量,用完之后归还。
创建很多个线程,都去获取信号量,没有获得信号量的线程都会阻塞。(保证拿到资源允许,没拿到资源阻塞)
能归还的线程都是前面获取到信号量的线程,其他没有获得线程都阻塞着。非阻塞的线程append后才release,这时候等待的线程被唤醒,才能pop,也就是没有获取信号量就不能pop,这是安全的。
信号量比计算列表长度好,线程安全,一旦多线程,线程安全是必须要的
锁,只允许同一个时间一个线程独占资源。是特殊的信号量,因为它的信号量计数器可以理解为初始值1。
信号量,可以多个线程访问这个资源,但这个资源数量有限,其他访问的只能等释放掉,
锁,可以看做特殊的信号量。
并不是所有的锁的用完就释放,这样就可以用with,但是有些是在一个函数使用锁,在另一个函数解开,所以就需要保证用过就释放
数据结构
在queue模块里有先进先出,FIFO的queue,queue类是线程安全的,适用于多线程间安全的交换数据。内部使用了Lock和Condition,用这两样东西来保证数据处理是安全的
但是实际有queue.qsize,当你读这个数据为0,get的时候不保证你一定能拿到这个数据。其实就是告诉你,if qsize >0 下面做操作(if语句是可以被打断的,防止打断,只能加锁)
所以因为这个if的原因,如果以qsize作为依据的话
类似下面这种情况,都有可能被打断,到底能不能put塞进去,还是get出来
读下queue的代码,condition里有个锁,称为mutex,一般称为互斥量,
这些都是condition,比如所有任务完成,
里面做了大量的判断
使用with语法是因为,not_empty是个condition,是个互斥量,就是一把锁
用这些技术就可以保证线程安全的
如果在多线程的时候,如果要使用数据,要想让多线程安全,这时候就需要去用队列用queue,但是queue有两个,
一个是多进程用的,一个是多线程用的
多线程 用queue.queue
面试经常问 GIL全局解释器锁
在进程级别的全局的锁
cpython中在解释器进程级别有一把锁,叫做GIL全局解释器锁。
GIL保证在Cpython进程,只有一个线程在执行字节码,即时是在多核的情况下,这把大锁,保证在同一时间,只能在某一个CPU核心上,当前进程的一个线程在执行
cpython中所谓的并行其实是假象。
cpython有了这个锁,就可以要求,线程调度在不同的cpu上,当下一个核心只能有一个线程在跑,真正干活的只有一个线程,其他的就是被迫等待,等待全局解释器锁,释放了另一个cpu里的,当前程序的线程
正好启动4个线程,如果调度4个线程,把4个线程分配到cpu核心上,自以为并行处理,在同一进程内的4个线程,分配到 了4个cpu核心上,并行处理是假象,因为有了这把GIL锁,在一个时刻,只能在某一个cpu运行一个线程,其他锁住了就被迫等待,但是因为切换很快,因为线程分时间片,由它来控制在某个cpu上运行
如果一个cpu也是并行,因为串行是一号执行完才是2号,但是这里就分时间片,交替运行,就是假并行 了,线程是交替并行在跑,给人的感觉是并行,有GIL解释器大锁,即时有多行,想要调度到多核上并行起来,因为有这个GIL大锁,也是做不到的,这是python被人诟病的地方(这个是多线程的问题,多进程其实就可以解决了,原来一个进程,可以开三个进程)
cpython中,IO密集,线程阻塞,就会调度其他线程
这个GIL大锁有特点,当你执行语句,到IO的时候(等待,IO完成才变成就绪)大量时间等IO,是浪费的,想快都快不了,这个时候就比较适合用python的多线程,
但是如果是cpu密集型,如果有需要大量计算cpu,想要几个核心上的线程一起,但是有GIL锁,在某个时刻在某一个核心上只能有一个线程在跑,其他核心上 线程就不能跑,这样大量时间就浪费了,这个时候就不太合适了
这样就会有一个现象,一个时刻只能在某个核心上只能一个线程在跑,其他核心上想并行的线程只能在等待,浪费时间。
还有一个现象,因为要唤醒那些线程,也需要时间,而且是抢GIL这把锁的,就会有某一个线程会频繁获得这个锁,其他线程一直在睡觉,因为唤醒线程需要时间,但是锁已经开始抢了,趁别人没醒,就又抢到了
如果cpu密集型,在cpython使用就会出现这样的情况
cpu密集型,使用多进程,不要使用多线程,绕开GIL
如果真的在意多线程问题,就用erlang和go语言
python中绝大多数内置数据结构的读、写操作都是原子操作。也就是不会出现读半截,写半截的问题,在这一点上看似线程安全的,但是这个数据不能说是线程安全的。
由于GIL的存在,python的内置数据类型,在多线程编程的时候就变成安全的了(因为读写不可拆分,在每个时刻,只有一个CPU核心在使用这个数据结构的对象),但是实际上它们本身不是线程安全类型(线程安全目前只有queue,因为有lock存在,内建数据类型其实不是线程安全带,因为在同一时刻只有一个核心在使用数据,还保证读写是必须完成的,不可分割)
在单线程使用GIL,效率是很高的,其实python鼓励你单线程,因为有携程,携程效率很高
下面有两个程序
下面的程序是cpu密集型的
不要写print,print要IO,线程就会被休息,到就绪的时候
运行起来
现在假设是只有主线程,执行完一个,执行下一个,把5个执行完
这是典型的多线程,cpu密集型,使用多线程来跑(本来想的是并行,但是实际是一个做完了下一个来做)
使用GIL锁之后,就算cpu密集型调度到不同核心上去,只能保证在某一个时刻某个核心上,会执行进程的某个线程,相当于分散到多个核心执行,但是还是一个单线程来执行
如果有IO语句,就会影响计算,从两个程序测试来看,cpython中多线程根本没有任何优势,和一个线程执行时间相当。因为GIL的存在,尤其是像上面的计算密集型程序,就不要使用python多线程做了。
第一段代码跑完,232秒
跑下一段代码
主线程一个,再开5个线程,其他辅助线程,加起来是7个
跑出来的结果应该和上面的单线程差不多
python中多线程的技术,event,lock,Rlock,condition,barrier,semaphore,
event是看状态变化,但是可以很多都看这个状态变化(event可以代替time.sleep,也可以通知其他人类似conditinon)
condition是通过notify方法来通知等待的,event是状态变化不通知你,你自己看。
对于锁来讲,最常用的是Lock,LOck是semaphore都是解决资源争用的问题,lock只有一个资源,大家都要抢,对于semaphore来讲,多个资源,大家一起抢,但是资源有限,抢完没有。这些用完都需要归还,归还就有空闲,空闲了就可以让别人使用。
唯一的用锁即可,多个资源用semaphore。
Rlock是可重入锁,递归锁,某一个线程获得锁之后的属主属于它,就可以重复进入,进几回release几次,不release,count无法清0,不清0,其他线程用不了,是线程相关的锁
barrier,我们做事情必须达到一个条件,谁做完了谁等,所有的做完了,才能做下一步,等参与方targets齐了,下一步,尤其是程序运行的时候可能需要(加载配置,建立连接)把这些初始服务做好了,程序才能起来。
semaphore的release有点问题,所以加边界boundedsemaphore,线程同步技术和多线程息息相关
谁快谁慢不好说,因为太接近了,运行了10亿次,运行5个,等于50亿次,这种时间是墙上时间,不是cpu真正分的时间,刚才用多线程的cpu密集型代码,相当于单线程的串行执行
因为GIL会导致CPU密集的时候,同一时刻,只能有一个线程在执行,而且只能在某一个核心上
多线程是操作系统提供的,要用,只不过python加了一把GIL锁,其他语言不一定有,所以使用方法上有区别。有GIL的存在,内置的数据结构就变的安全了