Python3学习(36)--多线程(二)

来源:互联网 发布:此网络受法律保护 编辑:程序博客网 时间:2024/05/23 17:30

我们思考一个问题,我们知道多线程,是用来并行执行任务的,任务可能是不一样的,比如上一篇讲的,任务也有可能是一样的(资源共享),那么问题来了


假设从信阳开往郑州的火车票总过有1000张(数据越大越能测出问题),在始发站信阳,总过有三个火车票代售点(代售点模拟的越多越能反映问题),这三个代售点,都进行售票的工作,想一想,这里面有什么漏洞吗?


资源共享:  火车票 10000张

动作        :   三个火车票代售点 同时访问这个资源,假设这个时候 票已经剩最后一张了

问题        :  某一时刻,售票窗口1的工作人员和窗口2的工作人员以及窗口3的工作人员同时查询火车票库存系统,返回的余票数量都是1


A、如果CPU运行很快,几乎没有时间差的(毫秒级别的都不行,得是微秒,纳秒级别的)话,会导致:


1张火车票,三个订单都成功(速度很快,业务层没问题),但是后台数据库肯定就有问题了:我勒个去,我就一张票,你们真狠,一下子操作了三次(库存量都减1,数据库肯定是要报警的),数据库不干了,导致虽然三个订单都成功了,但是打印车票的时候只能出一张!!!


B、假设CPU有细微时差的话,会导致:


速度略占优势的售票窗口成功出售和打印车票,但是另外两个工作人员就傻眼了:我勒个去,我明明刚才查的有票啊,怎么这时候就没了呢?  当然,这种情况是最理想的,因为,在客户准备支付的时候,窗口的工作人员已经发现没票了,操作终止!




上面的问题,我们可以用多线程来表示,每个窗口我们可以模拟成一个线程,售票的工作,我们可以定义成函数,当然是为每一个窗口定义一个相同的函数,然后,最关键的是,只有一个全局变量,那就是我们的火车票,假设,一开始火车票总量初始化为 10000张。


下面,我们利用demo,来模拟一下,多线程之间共享同一个资源存在的问题(潜在危险):


案例1:


#!/usr/bin/env Python3# -*- encoding:utf-8 -*-import threadingtickets  =  10000def sale1(name):    global tickets    while(tickets>0):        if(tickets>0):            tickets = tickets - 1            print('窗口【%s】剩余票数:%d--->%s' %(name,tickets,'售出一张票'))        else:            breakdef sale2(name):    global tickets    while(tickets>0):        if(tickets>0):            tickets = tickets - 1            print('窗口【%s】剩余票数:%d--->%s' %(name,tickets,'售出一张票'))        else:            breakdef sale3(name):    global tickets    while(tickets>0):        if(tickets>0):            tickets = tickets - 1            print('窗口【%s】剩余票数:%d--->%s' %(name,tickets,'售出一张票'))        else:            break        print('开往郑州的车票共有:',tickets,'张')         t1 = threading.Thread(target=sale1,args=('1',))t2 = threading.Thread(target=sale2,args=('2',))t3 = threading.Thread(target=sale3,args=('3',))t1.start()t2.start()t3.start()t1.join() #等待线程 使得子线程完全结束后,主线程才结束!!!t2.join()t3.join()if __name__ == '__main__':  #作为模块内部的demo调试、测试,非常方便        while(True):        if(tickets>0):            pass        else:            print('票已经卖完!')            break    print('余票共有:',tickets,'张')    

上面的demo,如果你只执行一次,可能发现不了问题


开始:



结尾:



上面是执行一次的结果(也有可能你一次执行就发现了问题所在),没问题,对吧,票都是按着编号来的,每卖出一张库存就减少一张,直到最后余票为0,提示票卖完,三个线程终止


但是,如果你多执行几次,你就会发现,打印的结果不一样,而且你执行的次数越多,这种不一样越加明显:


这里我们只放结尾的结果截图:




纳尼!!! 窗口1已经售完票了,为什么窗口3和窗口2还在继续售票!!!!

你们发现没,3和2只打印了一次,就不继续卖了,为什么呢?原因就是我们一开始说的,他们在查询系统的时候,系统给他们的返回余票数据确实就是 211和210,没毛病对吧,但是,这个时候1窗口一下子卖出了200多张票(假设1的电脑性能非常非常好),导致,3和2准备给客户支付的时候,发现,这时候,票已经没了,所以,2和3就终止了卖票行为,你们不相信这200多张票是1卖的嘛?  有图为证:




余票数量从212下面开始,都是1窗口卖出的,在这之前,其实3窗口也卖了一张票,所以,最后我们可以看到,3窗口那里打印了余票数量:211,很不幸,3的操作完全被1压下去了,2窗口也是!



如何解决上述这种问题呢?  这种问题可不是小问题啊,你想想,如果操作的是银行里面的存款(假设一个账户,有好几个途径可以同时进行存取操作),出现这种小插曲,多可怕,万一,后台对前台用户操作的数据无法进行准确的合法性判断的话,那么这个账户的钱是不是存在严重bug!亦或者,银行遭受到的投诉会越来越多!


有一种办法,就是,轮流访问资源,也就是一个人访问资源结束后,就告诉下一个人:好了,你可以访问了!这样依次类推。


这种方式有种弊端,就是工作效率会降低,主要体现在,每个线程访问共享资源的时候,首先自己要把这个资源占为己有,其他线程这个时候肯定是访问不了的,只能等待上一个线程告诉自己,可以访问了,下一个线程才能继续工作,如果上一个线程,没有告诉下面线程的话,会造成,下面的线程一直处于等待状态,这种时候,这些线程和死了就没什么区别!


好处就是,让共享资源始终处于一种安全的受保护的状态,这正是我们想要的!



上述文字,转换成demo思想就是,给访问资源的这种行为,加一把锁(行为之前),一旦访问资源的行为结束后,要解除锁!


在Python的模块threading中:


获得一个线程锁对象


lock     =  threading.Lock()


上锁: lock.acquire()

解锁: lock.release()


注意:这两个是成对出现的,有上锁必须要有对应的解锁操作,否则,会造成部分线程成为僵尸线程!!!


根据上面描述的,我们改进一下卖票方式,为每一个售票窗口加一把锁,最后别忘了释放锁:


案例2

#!/usr/bin/env Python3# -*- encoding:utf-8 -*-import threadingtickets  =  1000lock     =  threading.Lock()def sale1(name):    global tickets    while(tickets>0):        if(tickets>0):                lock.acquire()            tickets = tickets - 1            print('窗口【%s】剩余票数:%d--->%s' %(name,tickets,'售出一张票'))            lock.release()        else:            break        def sale2(name):    global tickets    while(tickets>0):        if(tickets>0):            lock.acquire()            tickets = tickets - 1            print('窗口【%s】剩余票数:%d--->%s' %(name,tickets,'售出一张票'))            lock.release()        else:            breakdef sale3(name):    global tickets    while(tickets>0):        if(tickets>0):            lock.acquire()            tickets = tickets - 1            print('窗口【%s】剩余票数:%d--->%s' %(name,tickets,'售出一张票'))            lock.release()        else:            break        print('开往郑州的车票共有:',tickets,'张')         t1 = threading.Thread(target=sale1,args=('1',))t2 = threading.Thread(target=sale2,args=('2',))t3 = threading.Thread(target=sale3,args=('3',))t1.start()t2.start()t3.start()t1.join()t2.join()t3.join()if __name__ == '__main__':        while(True):        if(tickets>0):            pass        else:            print('票已经卖完!')            break    print('余票共有:',tickets,'张')    


上面我们只讲了,锁应该加在操作的行为之前(加在操作之后就晚了),并没有对实际情况进行分析,我们看下上面的demo有没有问题,我们在while循环的if判断语句里面,也就是tickets = tickets -1操作之前加了一把锁,随后释放了这把锁,按理说没什么问题,对吧,但是,我们先看下结果再说:




无论你执行多少次,最终的结果都是  -2,为什么呢?


注意,你在售出一张票之前,是不是还要判断下,当前的余票是不是得大于0 啊,这个也是一个操作啊,而且这个操作也很重要啊,上面说了,锁要是加到操作行为之后的话,就晚了,这时候,你是不是恍然大悟,不管谁卖,最后肯定有一个窗口将票卖的只剩下0张,由于,我们没有在if判断之前加上锁,这就导致,必然有两个窗口会继续卖票,因为,他们并不知道当前的票是不是大于0啊,但是,当他们继续卖票的时候,才发现,卧槽,没票了,系统怎么不早说啊!


因此,我们只需调整下上述demo中锁的位置就行,改进后的最终demo如下:


案例3


#!/usr/bin/env Python3# -*- encoding:utf-8 -*-import threadingtickets  =  1000lock     =  threading.Lock()def sale1(name):    global tickets    while(tickets>0):        lock.acquire()#一定要注意线程锁的位置        if(tickets>0):                tickets = tickets - 1            print('窗口【%s】剩余票数:%d--->%s' %(name,tickets,'售出一张票'))            lock.release()        else:            lock.release()            break        def sale2(name):    global tickets    while(tickets>0):        lock.acquire()        if(tickets>0):            tickets = tickets - 1            print('窗口【%s】剩余票数:%d--->%s' %(name,tickets,'售出一张票'))            lock.release()        else:            lock.release()            breakdef sale3(name):    global tickets    while(tickets>0):        lock.acquire()        if(tickets>0):                tickets = tickets - 1            print('窗口【%s】剩余票数:%d--->%s' %(name,tickets,'售出一张票'))            lock.release()        else:            lock.release()            break        print('开往郑州的车票共有:',tickets,'张')         t1 = threading.Thread(target=sale1,args=('1',))t2 = threading.Thread(target=sale2,args=('2',))t3 = threading.Thread(target=sale3,args=('3',))t1.start()t2.start()t3.start()t1.join()t2.join()t3.join()if __name__ == '__main__':        while(True):        if(tickets>0):            pass        else:            print('票已经卖完!')            break    print('余票共有:',tickets,'张')    



噢啦,这一次无论你执行多少次,结果只有一个:




最后余票等于0的时候,任何窗口都停止继续售卖的行为,不可能出现,某个窗口还在傻傻的继续卖票。


使用多线程的时候一定要慎重!!!







原创粉丝点击