在python的多线程和多进程中,当我们需要对多线程或多进程的共享资源或对象进行修改操作时,往往会出现因cpu随机调度而导致结果和我们预期不一致的问题,这时就需要对线程或者进程加锁,以保证一个线程或进程在对共享对象进行修改时,其他的线程或进程无法访问这个对象,直至获取锁的线程的操作执行完毕后释放锁。所以,锁在多线程和多进程中起到一个同步的作用,以保护每个线程和进程必要操作的完整执行。
python中,多线程和多进程的异同点主要在两大处:1、由于GIL机制,多线程只能实现并发,但多进程可以实现并行;2、同一个进程中的多线程是共享内存的,线程没有独立的内存,但是进程有独立的内存,所以多进程中的每个子进程是有自己的独立内存的,所以在多进程中,其之间的同名变量并不会冲突,是独立的。第一点异同往往决定我们的任务时该使用多进程还是多线程,如果是I/O密集型任务,考虑多线程,如果是计算密集型任务,考虑多进程;第二点异同会引出本文的重点,即两者在加锁的方式上是不一样的。
首先讲一下加锁的机制,其是如何实现线程或进程保护的。这个实现的大致过程为:首先在需要同步的代码块前面加上lock.acquire()语句,表示需要先成功获取该锁,才能继续执行下面的代码,然后在需要同步的代码块后面加上lock.release()语句,表示释放该锁。所以,如果当一个线程或进程获取该锁,而且该锁没有被释放的话,那么其他的线程或进程是无法成功获取该锁的,从而也就没法执行下面的同步代码块,从而起到保护作用,直至释放该锁,其他的线程或进程才可以成功获取该锁,然后继续执行下面的代码块。通过上述,一个明显的基本前提是,不同线程或进程面对的锁必须是同一把锁,即同步代码块前后的lock对象必须是同一个,不然如果每个线程或进程有自己不同的锁,那么这个锁也就自然起不到保护作用了。
由于多线程共享内存,所以我们只要在该进程中创建一个锁,然后锁定相应的代码块即可,因为内存的共享,使得不同子线程面对的就是同一把锁,如下代码所示。这样可以使得每个线程对x进行修改后,x最后依然保持原值。
from threading import Thread,Lock
lock=Lock()
x=1
def f(x):
global x
lock.acquire()
x+=1
x-=1
lock.release()
if __name=='__main__':
t1=Thread(target=f,args=(1,))
t2=Thread(target=f,args=(2,))
t1.start()
t2.start()
t1.join()
t2.join()
但是在多进程中,由于每个子进程都会有自己的独立内存,所以如果按以上方式创建进程,那么实际上在函数f中的lock对象会根据LEGB原则读取到外面的lock对象,也就是说lock=Lock()这条语句也会被包括进每个子进程中,从而每个子进程不仅会在内存创建函数f,也会创建自己独立的锁对象,这样违背了一锁原则,从而无法起到同步作用。所以多进程中正确的加锁方式应该是先在主进程中创建一个锁,然后以参数的形式传给每个进程,这样相当于不同的子进程面对的是同一个锁。虽然实际上,每个子进程还是会在自己的内存中保存自己的锁对象,即如果获取每个进程的锁的id,那么其id还是不同的,说明其是不同的对象,但是因为这个锁的对象是在主进程创建的,子进程只是在自己的内存中复制了主进程中锁的状态,所以尽管不同子进程内存中也有自己的锁对象,但是这个锁对象的状态是一样的,这是最本质的,即我们实际上是需要不同线程或进程的锁是同步的,状态是一致,就可以认为是同一把锁,从而可以实现保护作用。具体的实现方式见如下代码。这样可以实现一个进程的两个print都执行完之后,另一个进程才会print,这一般在I/O里面多进程文件写入时会遇到。
from multiprocessing import Process,Lock
import time
def f(x,lock):
lock.acquire()
print(x+'1',id(lock))
time.sleep(5)
print(x+'2',id(lock))
lock.release()
if __name__=='__main__':
lock=Lock()
p1=Process(target=f,args=('x',lock))
p2=Process(target=f,args=('y',lock))
p1.start()
p2.start()
p1.join()
p2.join()
最后需要注意的一点是,创建多线程或多进程,都必须要先经过主模块判断,即必须在if __name__=='__main__':语句之后才行,这是为了保护资源的一种强制性机制。