1. 多线程编程与线程安全相关重要概念
在我的上篇博文 聊聊Python中的GIL 中,我们熟悉了几个特别重要的概念:GIL,线程,进程, 线程安全,原子操作。
以下是简单回顾,详细介绍请直接看聊聊Python中的GIL
GIL: Global Interpreter Lock,全局解释器锁。为了解决多线程之间数据完整性和状态同步的问题,设计为在任意时刻只有一个线程在解释器中运行。
线程:程序执行的最小单位。
进程:系统资源分配的最小单位。
线程安全:多线程环境中,共享数据同一时间只能有一个线程来操作。
原子操作:原子操作就是不会因为进程并发或者线程并发而导致被中断的操作。
还有一个重要的结论:当对全局资源存在写操作时,如果不能保证写入过程的原子性,会出现脏读脏写的情况,即线程不安全。Python的GIL只能保证原子操作的线程安全,因此在多线程编程时我们需要通过加锁来保证线程安全。
最简单的锁是互斥锁(同步锁),互斥锁是用来解决io密集型场景产生的计算错误,即目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据。
下面我们会来介绍如何使用互斥锁。
2. Threading.Lock实现互斥锁的简单示例
我们通过Threading.Lock()来实现锁。
以下是线程不安全的例子:
>>> importthreading>>> importtime>>> defsub1():globalcount
tmp=count
time.sleep(0.001)
count= tmp + 1time.sleep(2)>>> count =0>>> defverify(sub):globalcount
thread_list=[]for i in range(100):
t= threading.Thread(target=sub,args=())
t.start()
thread_list.append(t)for j inthread_list:
j.join()print(count)>>>verify(sub1)14
在这个例子中,我们把
count+=1
代替为
tmp =count
time.sleep(0.001)
count= tmp + 1
是因为,尽管count+=1是非原子操作,但是因为CPU执行的太快了,比较难以复现出多进程的非原子操作导致的进程不安全。经过代替之后,尽管只sleep了0.001秒,但是对于CPU的时间来说是非常长的,会导致这个代码块执行到一半,GIL锁就释放了。即tmp已经获取到count的值了,但是还没有将tmp + 1赋值给count。而此时其他线程如果执行完了count = tmp + 1, 当返回到原来的线程执行时,尽管count的值已经更新了,但是count = tmp + 1是个赋值操作,赋值的结果跟count的更新的值是一样的。最终导致了我们累加的值有很多丢失。
下面是线程安全的例子,我们可以用threading.Lock()获得锁
>>> count =0>>> defsub2():globalcountif lock.acquire(1):
#acquire()是获取锁,acquire(1)返回获取锁的结果,成功获取到互斥锁为True,如果没有获取到互斥锁则返回False
tmp=count
time.sleep(0.001)
count= tmp + 1time.sleep(2)
lock.release() 一系列操作结束之后需要释放锁>>> defver