上节讲了一下Python中的多线程,并做了演示。但肯定有小伙伴对线程的并发有疑惑,因为很多教程都说Python的线程没有并发能力。事实是这样吗?认真看了上节课程的小伙伴肯定注意到,它有并发能力。但为什么说没有呢,这就要来讲讲Python的锁。
首先,要说明一点。在Python的线程这里,有两把锁,分别是GIL锁和Lock锁。它们的功能和定位是不同的,很多人会在这个地方弄混,所以在使用线程时没发很好的利用线程并发。
P 1、GIL锁
GIL锁是Python解释器级别的,是用于控制解释器对多线程进行串行计算的锁。也就是说,它的存在在解释器中,将多个线程的字节码逐个进行计算,因为每个线程的计算时间短,所以才会有并发的感觉。下面是一个放慢的动画演示:
这个动画展示了Python中GIL锁的运行逻辑,当然CPU速度是很快的,所以我们感觉不到。
P 2、Lock锁
Lock是进程内部,控制各个线程关于某个资源共享的锁。就是说进程内部有个共享的资源,多个线程都可以访问,但为了避免冲突,当线程访问它时,先加锁来防止其它线程访问,当访问结束后,在解锁给其他线程访问。下面是三个线程轮流加锁访问变量的过程。
Lock锁的代码演示:
import timeimport threadinglock = threading.Lock()count = 0# 任务函数def myfunc(name): t0 = time.time() print(f' Thread {name} : Start') time.sleep(2) global count,lock lock.acquire() # 加锁 count+=1 lock.release() # 解锁 print(f' Thread {name} : {count=}') print(f' Thread {name} : End. Time={time.time()-t0}')print('Thread test :Start')t0 = time.time()th = []for i in range(3): # 创建线程 obj = threading.Thread(target=myfunc,args=(i,)) th.append(obj)for i in range(3): # 启动线程 th[i].start()for i in range(3): # 等待线程结束,此时对线程的join()方法可以运行,但其余的代码不可执行。 th[i].join()print(f'Thread test: End. Time={time.time()-t0}')
运行效果,如下:
Thread test : Start Thread 0 : Start Thread 1 : Start Thread 2 : Start Thread 0 : count=1 Thread 1 : count=2 Thread 1 : End. Time=2.0012946128845215 Thread 2 : count=3 Thread 2 : End. Time=2.001281976699829 Thread 0 : End. Time=2.002289295196533Thread test : End. Time=2.004284620285034
可以看到,线程0和线程1实际上存在打印变量的冲突,导致其数据的数据在同一行。打印前已经释放Lock锁了,所以打印出现了冲突。将myfunc函数代码做一下修改,就可以解决这个问题。如下:
# 任务函数def myfunc(name): t0 = time.time() print(f' Thread {name} : Start') time.sleep(2) global count,lock lock.acquire() # 加锁 count+=1 print(f' Thread {name} : {count=}') lock.release() # 解锁 print(f' Thread {name} : End. Time={time.time()-t0}')
再次运行测试代码,效果如下:
Thread test : Start Thread 0 : Start Thread 1 : Start Thread 2 : Start Thread 2 : count=1 Thread 2 : End. Time=2.002711296081543 Thread 1 : count=2 Thread 1 : End. Time=2.002711296081543 Thread 0 : count=3 Thread 0 : End. Time=2.0037083625793457Thread test : End. Time=2.005704402923584
这里要注意,Lock会造成其它线程等待资源占用,如果将过多的非数据访问操作放到加锁和解锁之间的代码,将会造成资源浪费(表现为闲置等待)。小伙伴可以尝试将上面的time.sleep(2)放到锁里看看,答案是运行时间会变成6s以上,是不是并行变串行了。
-------------------------它是数字世界里的一把杀猪刀
却总能巧夺天工
它的世界是纯粹0、1组合
却总能创造无尽幻想
......
本公众号关注数据价值分析、编程学习,将不定期更新社会热点数据分析结果、编程技巧,分享数据分析工具、方法、学习等内容,欢迎有兴趣的小伙伴加入。