Python文件锁portalocker模块

  在多进程/多线程的学习后,终于来到了“文件锁”这个概念阶段,文件锁的存在就是由于在多进程/线程操作时会对某个文件进行频繁修改,而导致读取与修改的数据产生不同步。典型场景有以下:

  • 进程1对文件A进行写入操作,写入一条记录a,持续时间时20s才能完成这个文件的写入。此时进程2在第5s时也开始对文件A进行修改,写入一条记录b,但只用了2s就写入了。这种情况下进程1完成写入记录a后,其实并不知道记录b已经在它之前就插入了,所以在进程1和进程2的”视角“中,这一份文件A的数据是不一样的。
  • 进程1对文件A进行写入操作,写入一条记录a,持续时间时20s才能完成这个文件的写入。此时进程2在第5s时,读取文件A,但由于记录a还没有被插入完成,所以在进程2的”视角“里,文件A中并没有记录a。

  以上两种典型情况在大并发量时都会导致不同进程中对同一份文件拿到的时不同的数据,此时我们就需要锁进行解决。由于python中对于unix系统存在fcntl标准模块,对于window具有

msvcrt标准模块,但这两个模块都只能在分别的操作系统中使用,而第三方包portalocker模块则对这两个系统做了兼容(这也是本文的主角)。

锁的基本理解

  在讲解包的使用之前,我们要明确锁的工作原理(无论是文件锁还是对象锁)。锁相当于授权,可以理解为只有当某个进程拿到了这个授权,才能执行之后的操作。而没有拿到授权就进行操作(对文件操作/ 对象操作)就会报错。因此在操作前我们需要拿到锁!对于锁我们还需要明确的时,加锁的进程,需要主动释放锁。在锁没有释放的情况下,其他进程时无法去获取到锁的,其他进程只能阻塞当前进程等待(或者抛出错误,当然我们也可以设定一个等待时间,超时了我就不等了)。

portalocker包的使用

  这个包的使用方式与常见的锁的使用方式都是一样的,最重要的就是加锁和释放锁。

  • portalocker.lock(文件权柄, portalocker.LOCK_SH):给文件赋予(尝试获得)共享锁,若不能获得则,阻塞当前进程,一般在只读文件时使用
  • portalocker.lock(文件权柄, portalocker.LOCK_EX):给文件赋予(尝试获得)独占锁,若不能获得则,阻塞当前进程, 一般在修改文件时使用
  • portalocker.lock(文件权柄, portalocker.LOCK_EX | portalocker.LOCK_NB) :  给文件赋予(尝试获得)独占锁,若不能获得则,则直接抛出异常

  我们需要注意的是以上三个方法都具有两个功能,第一个功能是给文件加锁,第二个功能是尝试获得这个文件的锁。我们直接从例子来看。由于我们需要模拟多进程情况,因此我们有三个.py文件,并且分别在不同终端中执行。

文件1:test.py

import pickle
import portalocker
import time


with open('test.pkl', mode="rb+") as f:
    portalocker.lock(f, portalocker.LOCK_EX) # 加锁 / 获取锁独占
    print("已增加锁")
## 以下代码在字典上新增元素 test = pickle.load(f) ## # 这里可以多test进行修改 ##
time.sleep(10) # 模拟阻塞 pickle.dump(test, f) portalocker.unlock(f) # 释放锁 print(f"已释放锁{test}")

  文件2:test2.py

import pickle
import time
import portalocker


with open('test.pkl', 'rb') as f:
    portalocker.lock(f, portalocker.LOCK_SH)  # 获取/加锁 共享锁
    test = pickle.load(f)
    time.sleep(5)  # 模拟阻塞
    print(test)
    portalocker.unlock(f)

  文件3:test3.py

import pickle
import portalocker

with open('test.pkl', 'rb') as f:
    portalocker.lock(f, portalocker.LOCK_SH)
    test = pickle.load(f)
    print(test)
    portalocker.unlock(f)

  分别解释下上述三个文件:文件1模拟对一个.pkl文件读写操作,并修改其结构,最后再将其保存,使用的是独占锁。文件2模拟对用一个.pkl文件进行只读,使的是共享锁。文件3是也是模拟对同一个.pkl文件进行只读,使的是也是共享锁。开始实验:

  实验一:短间隔(1s)先执行文件1,再执行文件2

   左边在执行时,右边开始了阻塞。直到当左边的独占锁释放了,右边才会获得锁,从而执行后续流程。我们可以知道当我某一个进程拿到了独占锁时,其余进程的共享锁、独占锁都会被阻塞等待这个独占锁的释放。这就解决了当我们在更改某个文件时,其他进程不会拿到老版本的文件数据。只有当文件锁释放后,其他进程才拿到锁,进而执行后续的操作。注意这个我们在文件2中是在读取文件前(第八行)去请求锁(第七行),而不是早操作后,因为如果不请求锁而直接操作,就会直接抛出portalocker.LockException异常。

  实验二:短间隔(1s)先执行文件2,再执行文件1

   上图是在执行中的截图,我们可以看到,当文件2执行完成后,文件1才开始执行。说明当一个进程获得了某个文件的共享锁,其他进程是无法获得该文件的独占锁的,需要等到共享锁释放后,才能对这个文件拿到独占锁。

  实验三:短间隔(1s)先执行文件2,再执行文件3

   上图在文件2执行后,文件3仍可以正常执行,说明当一个进程给某个文件加上了共享锁,其余进程仍可以拿到共享锁,并执行自己后续的代码。这就解决了在读取文件时我们不用等待其他读取进程释放锁,这样就极大的提升了效率。

超时时间

  前面提到了,我们可以设置等待锁的超时时间,我们在使用以下方式时可以使用timeout功能来防止死锁。

import pickle
import portalocker
import time

with portalocker.Lock('test.pkl', mode="rb+", timeout=100) as f:
    print("已增加锁")
    test = pickle.load(f)
    time.sleep(10)
    pickle.dump(test, f)

    print(f"已释放锁{test}")

  上面代码是在文件1的基础上改进的(其功能完全相同),我们不使用open来打开文件,而使用 portalocker.Lock 来打开文件。这时我们可以不用显式的进行上锁和解锁,并且我们还可以使用timeout来设定最长等待获取锁的时间(如果在这个时间后还没有获得锁,那么就抛出异常,从而防止了死锁的存在)。

  不过我们需要注意的是,前面三个portalocker.lock方法都不能设定timeout。并且portalocker.Lock()默认使用的只有独占锁,并且无法设定为共享锁。因此如果你有对共享锁也想使用timeout的方式来实现防止死锁,就只能自己写计时了。

 

posted @ 2023-11-24 18:23  Circle_Wang  阅读(374)  评论(0编辑  收藏  举报