From 05605fbe45445483573e3ac8306e852a36ac7800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Larivi=C3=A8re?= Date: Wed, 8 Jul 2015 01:24:35 -0400 Subject: [PATCH] Initial implementation --- .gitignore | 35 ++++++ .gitlab-ci.yml | 3 + BUILDME | 220 +++++++++++++++++++++++++++++++++++++ BUILDME.bat | 2 + BUILDME.py | 21 ++++ README | 1 + README.md | 78 +++++++++++++ src/RWLock.py | 267 +++++++++++++++++++++++++++++++++++++++++++++ src/RWLock_Test.py | 141 ++++++++++++++++++++++++ src/__init__.py | 2 + 10 files changed, 770 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100755 BUILDME create mode 100755 BUILDME.bat create mode 100755 BUILDME.py create mode 120000 README create mode 100644 README.md create mode 100644 src/RWLock.py create mode 100755 src/RWLock_Test.py create mode 100644 src/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be14886 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.dll +*.exe + +# C extensions +*.so + +# Coverage report +.coverage +htmlcov/ +coverage.xml + +# Packages # +############ +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# OS generated files # +###################### +.DS_Store +.DS_Store? +ehthumbs.db +Thumbs.db +Desktop.ini +*~ +*.lock + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..f70462b --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,3 @@ +BUILDME: + script: "./BUILDME" + diff --git a/BUILDME b/BUILDME new file mode 100755 index 0000000..8f85397 --- /dev/null +++ b/BUILDME @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + This is a script to build the project + + It is using template_BUILDME_python3 v3 + + This script: + - Should be named "BUILDME" + - Should be present in the project root folder + - Should be executable (chmod +x BUILDME) + - Should contains all the project build process steps implemented into the function build (see below) +""" + +def build(p_Src, p_Tmp, p_Out): + """ + Build the project + @param p_Src The project root folder, the build steps shall only access this folder in read only mode + @param p_Tmp A folder where the build steps are free to write temporary file/folder + @param p_Out A folder where the build result should be written to + @returns Exit code 0 on success or raise an Exception if something failed + """ + import os + import shutil + import subprocess + import sys + + #Running unit test + assert sys.executable is not None + assert 0 < len(sys.executable) + + for c_CurrDestination in [p_Tmp]: + shutil.copy(os.path.join(p_Src, "src", "RWLock.py"), c_CurrDestination) + shutil.copy(os.path.join(p_Src, "src", "RWLock_Test.py"), c_CurrDestination) + shutil.copy(os.path.join(p_Src, "src", "__init__.py"), c_CurrDestination) + try: + print(subprocess.check_output([sys.executable, os.path.join(p_Tmp, "RWLock_Test.py")], stderr=subprocess.STDOUT, universal_newlines=True)) + except subprocess.CalledProcessError as e_CalledProcessError: + print(e_CalledProcessError.output) + raise + #Writting the output + for c_CurrDestination in [p_Out]: + shutil.copy(os.path.join(p_Src, "src", "RWLock.py"), c_CurrDestination) + shutil.copy(os.path.join(p_Src, "src", "RWLock_Test.py"), c_CurrDestination) + shutil.copy(os.path.join(p_Src, "src", "__init__.py"), c_CurrDestination) + return 0 + +###Settings + +####Validate that the SRC folder is not altered while the build steps are in progress. (Default = True) +#S_SrcHashValidation = False + +######################################################## +######################################################## +############################ +############################ Copy this template build executable to your own project root folder +############################ Anything above this line is considered part of your project +############################ Anything below this line is part of the BUILDME project and is licensed under GPLv3 +############################ Note: You don't have to edit anything below this line, if you have to, please fill an issue at https://github.com/elarivie/BUILDME +############################ +######################################################## +######################################################## +def main(p_Args): + import argparse + parser = argparse.ArgumentParser( + description='Build the project' + , epilog=''' +Exit code 0 if build is successfull +''' + ) + import os + parser.add_argument('-s', '--source', help='source folder (Default: Current working directory)', action='store', default=os.getcwd()) + parser.add_argument('-t', '--temp', help='temporary folder (Default: new temporary directory in the system temporary directory)', action='store', default=None) + parser.add_argument('-o', '--output', help='output folder, shall not already exist (Default: new temporary directory in TEMPFOLDER)', action='store', default=None) + parser.add_argument('-O', '--Output', help='output folder, shall already exist (Default: --output)', action='store', default=None) + parser.add_argument('-V', '--version', action='version', version='build 1.1') + c_Args = parser.parse_args(p_Args) + def _actualpath(p_Path): + return None if p_Path is None else os.path.normcase(os.path.realpath(os.path.normpath(p_Path))) + def _processDirectoryHash(p_Path): + """ + Walk the directory tree and process a hash + @p_Path The directory to process a hash base on its content + @return The Hash (sha512) + """ + if p_Path is None: + return 0 + import hashlib + c_CheckSum = hashlib.sha512() + for c_Root, c_Dirs, c_Files in os.walk(p_Path, followlinks=False): + for c_Name in c_Files: + c_CheckSum.update(c_Name.encode('utf-8'))#Use the file name + c_CurrPath = os.path.join(c_Root, c_Name) + #c_CheckSum.update(str(os.path.getsize(c_CurrPath)).encode('utf-8'))#Use the file size + with open(c_CurrPath, 'rb') as c_CurrentFile: + while True: + v_Buffer = c_CurrentFile.read(8192) + if len(v_Buffer) > 0: + c_CheckSum.update(v_Buffer)#Use the file content + else: + break + for c_Name in c_Dirs: + c_CheckSum.update(c_Name.encode('utf-8'))#Use the directory name + c_CurrPath = os.path.join(c_Root, c_Name) + if not os.path.islink(c_CurrPath): + c_CheckSum.update(_processDirectoryHash(c_CurrPath).encode('utf-8'))#Use the directory content + return c_CheckSum.hexdigest() + c_ActualPathSrc = _actualpath(c_Args.source) + c_ActualPathTmp = _actualpath(c_Args.temp) + c_ActualPathOut = _actualpath(c_Args.output) + v_NeedToCreateOut = True + if c_ActualPathOut is None: + c_ActualPathOut = _actualpath(c_Args.Output) + v_NeedToCreateOut = False + assert (c_ActualPathOut is None) or (os.path.isdir(c_ActualPathOut)), "The output folder has to be a folder" + import tempfile + v_BuildResult = None + c_OrigCWD = os.getcwd() + v_TmpPath = None + v_BuildError = True + try: + with tempfile.TemporaryDirectory(suffix='', prefix='', dir=c_ActualPathTmp) as c_TmpFolder: + v_TmpPath = _actualpath(c_TmpFolder)#This is needed since on the OS Windows which is case insensitive it may create invalid result when comparing path + try: + if c_ActualPathOut is None: + v_NeedToCreateOut = False + c_ActualPathOut = tempfile.mkdtemp(suffix='', prefix='', dir=v_TmpPath) + if c_ActualPathSrc is None: + c_ActualPathSrc = tempfile.mkdtemp(suffix='', prefix='', dir=v_TmpPath) + assert os.path.isdir(c_ActualPathSrc), "The source folder has to be a folder" + assert (c_ActualPathTmp is None) or os.path.isdir(c_ActualPathTmp), "The temporary folder has to be a folder" + assert (c_ActualPathTmp is None) or not c_ActualPathTmp.startswith(c_ActualPathSrc + os.sep), "The temporary folder cannot be within the source folder" + assert not c_ActualPathOut.startswith(c_ActualPathSrc + os.sep), "The output folder cannot be within the source folder" + assert not c_ActualPathSrc.startswith(c_ActualPathOut + os.sep), "The source folder cannot be within the output folder" + assert (c_ActualPathTmp is None) or not c_ActualPathTmp.startswith(c_ActualPathOut + os.sep), "The temporary folder cannot be within the output folder" + if v_NeedToCreateOut: + os.makedirs(c_ActualPathOut, exist_ok=False) + assert os.path.isdir(c_ActualPathOut), "The output folder has to be a folder" + v_DoSrcHashValidation = True + try: + v_DoSrcHashValidation = S_SrcHashValidation + except NameError: + pass + c_SrcHash = _processDirectoryHash(c_ActualPathSrc) if v_DoSrcHashValidation else 0 + c_BuildTmpFolder = _actualpath(tempfile.mkdtemp(suffix='', prefix='', dir=v_TmpPath)) + os.chdir(c_BuildTmpFolder)#Make sure that the current working directory is the Temp folder before doing the build steps + v_BuildResult = build(c_ActualPathSrc, c_BuildTmpFolder, c_ActualPathOut) + #From this point, the only thing left to do is to delete the temp folder + v_BuildError = False + finally: + if _actualpath(os.getcwd()).startswith(v_TmpPath + os.sep): + os.chdir(c_OrigCWD)#Get out of the temp folder since it is about to be deleted + except Exception: + if v_BuildError: + raise + else: + pass#Problem cleaning temp folder... will be handled in the finally block + finally: + if v_TmpPath is not None and os.path.isdir(v_TmpPath): + #Failed to remove temp folder + # Known possible cause: + # - On OS Windows if the Tmp folder contains element with long file path + # Will now attempt other strategies to to clean up as much as possible + # Since the temp folder destiny is to be deleted we can alter its content + # Walk multiple time the temp directory: + # - Remove each file and folder specifically + # - If delete is not possible, rename them with a shorter name + # The objective is to: + # - Reduce as much as possible the file path length + # - Keep only the problematic files/folder + try: + v_SawImprovement = True + while v_SawImprovement: + v_SawImprovement = False + for c_Root, c_Dirs, c_Files in os.walk(v_TmpPath, topdown=False): + for c_Name in c_Files: + try: + os.unlink(os.path.join(c_Root, c_Name)) + v_SawImprovement = True + except Exception: + try: + os.replace(os.path.join(c_Root, c_Name), os.path.join(c_Root, "A"))#Rename to a single character name + v_SawImprovement = True + except Exception: + pass#Will retry later + for c_Name in c_Dirs: + try: + os.rmdir(os.path.join(c_Root, c_Name)) + v_SawImprovement = True + except Exception: + try: + os.replace(os.path.join(c_Root, c_Name), os.path.join(c_Root, "A"))#Rename to a single character name + v_SawImprovement = True + except Exception: + pass#Will retry later + os.rmdir(v_TmpPath)#Give it a try now that the content might have been deleted + except Exception: + pass#This is not a reason to fail the build yet... + if os.path.exists(v_TmpPath): + #Strangely the temp folder is still present... + #No known reason to reach this area but will try other strategies anyway + import shutil + try: + shutil.rmtree(v_TmpPath)#Try a deep delete of the temp folder. + except Exception: + pass#This is not a reason to fail the build yet... + if os.path.exists(v_TmpPath): + import time + time.sleep(5)#In last resort give five seconds to other processes and the OS to breath in case they need to cleanly release their handle on the temp folder + shutil.rmtree(v_TmpPath)#Last chance to delete temp, if it does not work, it will throw an exception to the user... we did our best. + assert not os.path.exists(v_TmpPath), "Temp folder could not be cleaned: " + str(v_TmpPath) + assert c_SrcHash == (_processDirectoryHash(c_ActualPathSrc) if v_DoSrcHashValidation else 0), "Source folder was modified by the build process" + return v_BuildResult + +if __name__ == '__main__': + import sys + sys.dont_write_bytecode = True + sys.exit(main(sys.argv[1:])) + diff --git a/BUILDME.bat b/BUILDME.bat new file mode 100755 index 0000000..174a8c8 --- /dev/null +++ b/BUILDME.bat @@ -0,0 +1,2 @@ +python -B BUILDME %* +EXIT /B %ERRORLEVEL% diff --git a/BUILDME.py b/BUILDME.py new file mode 100755 index 0000000..8de3bd1 --- /dev/null +++ b/BUILDME.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# This is an entry point for the file BUILDME +# The current file: +# Should be named "BUILDME.py" +# Should be present in the project root folder + +def main(p_Args): + import os + import subprocess + import sys + c_Args = [sys.executable, "-B", "BUILDME"] + c_Args.extend(p_Args) + return subprocess.call(c_Args) + +if __name__ == '__main__': + import sys + sys.dont_write_bytecode = True + sys.exit(main(sys.argv[1:])) + diff --git a/README b/README new file mode 120000 index 0000000..42061c0 --- /dev/null +++ b/README @@ -0,0 +1 @@ +README.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..396466c --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +pyReaderWriterLock +================== + +**A python implementation of the three Reader-Writer problems.** + +Not only does it implement the reader-writer problems, it also support the python lock interface which includes support for timeouts. + +For reading about the theory behind the reader-writer problems refer to [Wikipedia](https://wikipedia.org/wiki/Readers–writers_problem). + +# Usage + +1. First initialize a new lock base on your access priority need: + +**Reader priority** (*aka First readers-writers problem*) + +```python +import RWLock +a = RWLock.RWLockRead() +``` + +**Writer priority** (*aka Second readers-writers problem*) + +```python +import RWLock +a = RWLock.RWLockWrite() +``` + +**Fair priority** (*aka Third readers-writers problem*) + +```python +import RWLock +a = RWLock.RWLockFair() +``` + +2. Use it in multiple threads: + +## Pythonic usage example + +``` +with a.genRlock(): + #Read stuff +with a.genWlock(): + #Write stuff +``` + +## Advanced Usage example +``` +b = a.genWlock() +if b.acquire(blocking=1, timeout=5): + #Do stuff + b.release() +``` + +## Live example +Refer to the file [RWLock_Test.py](src/RWLock_Test.py) which can be directly called, it has above 90% line coverage of [RWLock.py](src/RWLock.py). + +The tests can be initiated by doing + +```bash +./RWLock_Test.py +``` + +# Build +This project use the [BUILDME](https://github.com/elarivie/BUILDME) interface, you may therefore build the project by simply doing: +```bash +./BUILDME +``` + +Contribute +---- +You are the welcome to contribute (Welcome in the open source world): +* Bug/Suggestion/Comment + +Contact +---- +* Project: [GitHub](https://github.com/elarivie/pyReaderWriterLock) +* Éric Larivière + diff --git a/src/RWLock.py b/src/RWLock.py new file mode 100644 index 0000000..612aba4 --- /dev/null +++ b/src/RWLock.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Read Write Lock +""" + +import threading +import time + +class RWLockRead(object): + """ + A Read/Write lock giving preference to Reader + """ + def __init__(self): + self.V_ReadCount = 0 + self.A_Resource = threading.Lock() + self.A_LockReadCount = threading.Lock() + class _aReader(object): + def __init__(self, p_RWLock): + self.A_RWLock = p_RWLock + self.V_Locked = False + + def acquire(self, blocking=1, timeout=-1): + p_TimeOut = None if (blocking and timeout < 0) else (timeout if blocking else 0) + c_DeadLine = None if p_TimeOut is None else (time.time() + p_TimeOut) + if not self.A_RWLock.A_LockReadCount.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + return False + self.A_RWLock.V_ReadCount += 1 + if self.A_RWLock.V_ReadCount == 1: + if not self.A_RWLock.A_Resource.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + self.A_RWLock.V_ReadCount -= 1 + self.A_RWLock.A_LockReadCount.release() + return False + self.A_RWLock.A_LockReadCount.release() + self.V_Locked = True + return True + + def release(self): + if not self.V_Locked: raise RuntimeError("cannot release un-acquired lock") + self.V_Locked = False + self.A_RWLock.A_LockReadCount.acquire() + self.A_RWLock.V_ReadCount -= 1 + if self.A_RWLock.V_ReadCount == 0: + self.A_RWLock.A_Resource.release() + self.A_RWLock.A_LockReadCount.release() + def locked(self): + return self.V_Locked + def __enter__(self): + self.acquire() + def __exit__(self, p_Type, p_Value, p_Traceback): + self.release() + class _aWriter(object): + def __init__(self, p_RWLock): + self.A_RWLock = p_RWLock + self.V_Locked = False + def acquire(self, blocking=1, timeout=-1): + self.V_Locked = self.A_RWLock.A_Resource.acquire(blocking, timeout) + return self.V_Locked + def release(self): + if not self.V_Locked: raise RuntimeError("cannot release un-acquired lock") + self.V_Locked = False + self.A_RWLock.A_Resource.release() + def locked(self): + return self.V_Locked + def __enter__(self): + self.acquire() + def __exit__(self, p_Type, p_Value, p_Traceback): + self.release() + def genRlock(self): + """ + Generate a reader lock + """ + return RWLockRead._aReader(self) + def genWlock(self): + """ + Generate a writer lock + """ + return RWLockRead._aWriter(self) + +class RWLockWrite(object): + """ + A Read/Write lock giving preference to Writer + """ + def __init__(self): + self.V_ReadCount = 0 + self.V_WriteCount = 0 + self.A_LockReadCount = threading.Lock() + self.A_LockWriteCount = threading.Lock() + self.A_LockReadEntry = threading.Lock() + self.A_LockReadTry = threading.Lock() + self.A_Resource = threading.Lock() + class _aReader(object): + def __init__(self, p_RWLock): + self.A_RWLock = p_RWLock + self.V_Locked = False + def acquire(self, blocking=1, timeout=-1): + p_TimeOut = None if (blocking and timeout < 0) else (timeout if blocking else 0) + c_DeadLine = None if p_TimeOut is None else (time.time() + p_TimeOut) + if not self.A_RWLock.A_LockReadEntry.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + return False + if not self.A_RWLock.A_LockReadTry.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + self.A_RWLock.A_LockReadEntry.release() + return False + if not self.A_RWLock.A_LockReadCount.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + self.A_RWLock.A_LockReadTry.release() + self.A_RWLock.A_LockReadEntry.release() + return False + self.A_RWLock.V_ReadCount += 1 + if (self.A_RWLock.V_ReadCount == 1): + if not self.A_RWLock.A_Resource.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + self.A_RWLock.A_LockReadTry.release() + self.A_RWLock.A_LockReadEntry.release() + self.A_RWLock.V_ReadCount -= 1 + self.A_RWLock.A_LockReadCount.release() + return False + self.A_RWLock.A_LockReadCount.release() + self.A_RWLock.A_LockReadTry.release() + self.A_RWLock.A_LockReadEntry.release() + self.V_Locked = True + return True + def release(self): + if not self.V_Locked: raise RuntimeError("cannot release un-acquired lock") + self.V_Locked = False + self.A_RWLock.A_LockReadCount.acquire() + self.A_RWLock.V_ReadCount -= 1 + if (self.A_RWLock.V_ReadCount == 0): + self.A_RWLock.A_Resource.release() + self.A_RWLock.A_LockReadCount.release() + def locked(self): + return self.V_Locked + def __enter__(self): + self.acquire() + def __exit__(self, p_Type, p_Value, p_Traceback): + self.release() + class _aWriter(object): + def __init__(self, p_RWLock): + self.A_RWLock = p_RWLock + self.V_Locked = False + def acquire(self, blocking=1, timeout=-1): + p_TimeOut = None if (blocking and timeout < 0) else (timeout if blocking else 0) + c_DeadLine = None if p_TimeOut is None else (time.time() + p_TimeOut) + if not self.A_RWLock.A_LockWriteCount.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + return False + self.A_RWLock.V_WriteCount += 1 + if (self.A_RWLock.V_WriteCount == 1): + if not self.A_RWLock.A_LockReadTry.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + self.A_RWLock.V_WriteCount -= 1 + self.A_RWLock.A_LockWriteCount.release() + return False + self.A_RWLock.A_LockWriteCount.release() + if not self.A_RWLock.A_Resource.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + self.A_RWLock.A_LockWriteCount.acquire() + self.A_RWLock.V_WriteCount -= 1 + if self.A_RWLock.V_WriteCount == 0: + self.A_RWLock.A_LockReadTry.release() + self.A_RWLock.A_LockWriteCount.release() + return False + self.V_Locked = True + return True + def release(self): + if not self.V_Locked: raise RuntimeError("cannot release un-acquired lock") + self.V_Locked = False + self.A_RWLock.A_Resource.release() + self.A_RWLock.A_LockWriteCount.acquire() + self.A_RWLock.V_WriteCount -= 1 + if (self.A_RWLock.V_WriteCount == 0): + self.A_RWLock.A_LockReadTry.release() + self.A_RWLock.A_LockWriteCount.release() + def locked(self): + return self.V_Locked + def __enter__(self): + self.acquire() + def __exit__(self, p_Type, p_Value, p_Traceback): + self.release() + def genRlock(self): + """ + Generate a reader lock + """ + return RWLockWrite._aReader(self) + def genWlock(self): + """ + Generate a writer lock + """ + return RWLockWrite._aWriter(self) + +class RWLockFair(object): + """ + A Read/Write lock giving fairness to both Reader and Writer + """ + def __init__(self): + self.V_ReadCount = 0 + self.A_LockReadCount = threading.Lock() + self.A_LockRead = threading.Lock() + self.A_LockWrite = threading.Lock() + class _aReader(object): + def __init__(self, p_RWLock): + self.A_RWLock = p_RWLock + self.V_Locked = False + def acquire(self, blocking=1, timeout=-1): + p_TimeOut = None if (blocking and timeout < 0) else (timeout if blocking else 0) + c_DeadLine = None if p_TimeOut is None else (time.time() + p_TimeOut) + if not self.A_RWLock.A_LockRead.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + return False + if not self.A_RWLock.A_LockReadCount.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + self.A_RWLock.A_LockRead.release() + return False + self.A_RWLock.V_ReadCount += 1 + if self.A_RWLock.V_ReadCount == 1: + if not self.A_RWLock.A_LockWrite.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + self.A_RWLock.V_ReadCount -= 1 + self.A_RWLock.A_LockReadCount.release() + self.A_RWLock.A_LockRead.release() + return False + self.A_RWLock.A_LockReadCount.release() + self.A_RWLock.A_LockRead.release() + self.V_Locked = True + return True + def release(self): + if not self.V_Locked: raise RuntimeError("cannot release un-acquired lock") + self.V_Locked = False + self.A_RWLock.A_LockReadCount.acquire() + self.A_RWLock.V_ReadCount -= 1 + if self.A_RWLock.V_ReadCount == 0: + self.A_RWLock.A_LockWrite.release() + self.A_RWLock.A_LockReadCount.release() + def locked(self): + return self.V_Locked + def __enter__(self): + self.acquire() + def __exit__(self, p_Type, p_Value, p_Traceback): + self.release() + class _aWriter(object): + def __init__(self, p_RWLock): + self.A_RWLock = p_RWLock + self.V_Locked = False + def acquire(self, blocking=1, timeout=-1): + p_TimeOut = None if (blocking and timeout < 0) else (timeout if blocking else 0) + c_DeadLine = None if p_TimeOut is None else (time.time() + p_TimeOut) + if not self.A_RWLock.A_LockRead.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + return False + if not self.A_RWLock.A_LockWrite.acquire(blocking=1, timeout=-1 if c_DeadLine is None else max(0, c_DeadLine - time.time())): + self.A_RWLock.A_LockRead.release() + return False + self.V_Locked = True + return True + def release(self): + if not self.V_Locked: raise RuntimeError("cannot release un-acquired lock") + self.V_Locked = False + self.A_RWLock.A_LockWrite.release() + self.A_RWLock.A_LockRead.release() + def locked(self): + return self.V_Locked + def __enter__(self): + self.acquire() + def __exit__(self, p_Type, p_Value, p_Traceback): + self.release() + def genRlock(self): + """ + Generate a reader lock + """ + return RWLockFair._aReader(self) + def genWlock(self): + """ + Generate a writer lock + """ + return RWLockFair._aWriter(self) + diff --git a/src/RWLock_Test.py b/src/RWLock_Test.py new file mode 100755 index 0000000..471d2be --- /dev/null +++ b/src/RWLock_Test.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import unittest + +class TestStringMethods(unittest.TestCase): + + def setUp(self): + self.V_Value = 0 + import RWLock + self.C_RWLockInstance = [RWLock.RWLockRead(), RWLock.RWLockWrite(), RWLock.RWLockFair()] + + def test_SingleAccess(self): + print("test_SingleAccess") + for c_CurrLock in self.C_RWLockInstance: + with self.subTest(c_CurrLock): + for c_CurrLockX in [c_CurrLock.genRlock(), c_CurrLock.genWlock()]: + self.assertFalse(c_CurrLockX.locked()) + with c_CurrLockX: + self.assertTrue(c_CurrLockX.locked()) + self.assertFalse(c_CurrLockX.locked()) + for c_Params in [[0, -1], [1, -1], [1, 0], [1, 1]]: + with self.subTest([c_CurrLock, c_CurrLockX, c_Params]): + self.assertTrue(c_CurrLockX.acquire(blocking=c_Params[0], timeout=c_Params[1])) + self.assertTrue(c_CurrLockX.locked()) + c_CurrLockX.release() + self.assertFalse(c_CurrLockX.locked()) + with self.assertRaises(RuntimeError): + c_CurrLockX.release() + + def test_MultiRead(self): + print("test_MultiRead") + for c_CurrLock in self.C_RWLockInstance: + with self.subTest(c_CurrLock): + c_LockR0 = c_CurrLock.genRlock() + c_LockR1 = c_CurrLock.genRlock() + c_LockW1 = c_CurrLock.genWlock() + + self.assertTrue(c_LockR0.acquire()) + self.assertTrue(c_LockR1.acquire()) + self.assertTrue(c_LockR0.locked()) + self.assertTrue(c_LockR1.locked()) + self.assertFalse(c_LockW1.acquire(blocking=True, timeout=1)) + self.assertFalse(c_LockW1.locked()) + c_LockR1.release() + self.assertTrue(c_LockR0.locked()) + self.assertFalse(c_LockR1.locked()) + self.assertFalse(c_LockW1.acquire(blocking=True, timeout=1)) + self.assertFalse(c_LockW1.locked()) + c_LockR0.release() + self.assertFalse(c_LockR0.locked()) + self.assertFalse(c_LockR1.locked()) + self.assertTrue(c_LockW1.acquire(blocking=True, timeout=1)) + self.assertTrue(c_LockW1.locked()) + self.assertFalse(c_LockR0.acquire(blocking=True, timeout=1)) + self.assertFalse(c_LockR0.locked()) + c_LockW1.release() + self.assertFalse(c_LockW1.locked()) + + def test_MultiWrite(self): + print("test_MultiWrite") + for c_CurrLock in self.C_RWLockInstance: + with self.subTest(c_CurrLock): + c_LockR0 = c_CurrLock.genRlock() + c_LockR1 = c_CurrLock.genRlock() + c_LockW0 = c_CurrLock.genWlock() + c_LockW1 = c_CurrLock.genWlock() + self.assertTrue(c_LockW0.acquire()) + self.assertTrue(c_LockW0.locked()) + self.assertFalse(c_LockW1.acquire(blocking=False)) + self.assertFalse(c_LockW1.locked()) + self.assertFalse(c_LockW1.acquire(blocking=True, timeout=1)) + self.assertFalse(c_LockW1.locked()) + c_LockW0.release() + + self.assertTrue(c_LockR0.acquire()) + self.assertTrue(c_LockR0.locked()) + self.assertFalse(c_LockW1.acquire(blocking=False)) + self.assertFalse(c_LockW1.locked()) + self.assertFalse(c_LockW1.acquire(blocking=True, timeout=1)) + self.assertFalse(c_LockW1.locked()) + self.assertTrue(c_LockR1.acquire()) + self.assertTrue(c_LockR1.locked()) + c_LockR0.release() + c_LockR1.release() + self.assertFalse(c_LockR0.locked()) + self.assertFalse(c_LockR1.locked()) + + def test_MultiThread(self): + s_PeriodSec = 60 + print("test_MultiThread (" + str(s_PeriodSec * len(self.C_RWLockInstance)) + " sec)") + def writer1(): + c_EnterTime = time.time() + c_Lockw1 = c_CurrLock.genWlock() + while time.time() - c_EnterTime <= s_PeriodSec: + time.sleep(0) + c_Lockw1.acquire() + v_Temp = self.V_Value + self.V_Value += 1 + assert self.V_Value == (v_Temp + 1) + time.sleep(0.1) + c_Lockw1.release() + def reader1(): + c_EnterTime = time.time() + c_Lockr1 = c_CurrLock.genRlock() + while time.time() - c_EnterTime <= s_PeriodSec: + time.sleep(0) + with c_Lockr1: + vv_Value = self.V_Value + time.sleep(2) + assert vv_Value == self.V_Value + import threading + import time + c_ValueEnd = [] + for c_CurrLock in self.C_RWLockInstance: + with self.subTest(c_CurrLock): + threadsarray = [] + for i in range(10): + threadsarray.append(threading.Thread(group=None, target=writer1, name="writer" + str(i), daemon=False)) + threadsarray.append(threading.Thread(group=None, target=reader1, name="reader" + str(i), daemon=False)) + for c_CurrThread in threadsarray: + c_CurrThread.start() + for c_CurrThread in threadsarray: + while c_CurrThread.is_alive(): + time.sleep(0.5) + c_ValueEnd.append(self.V_Value) + with self.subTest(c_ValueEnd): + self.assertEqual(len(c_ValueEnd), 3) + self.assertGreater(c_ValueEnd[0], 0) + self.assertGreater(c_ValueEnd[1], c_ValueEnd[0]) + self.assertGreater(c_ValueEnd[2], c_ValueEnd[1]) + c_PriorityR = c_ValueEnd[0] - 0 + c_PriorityW = c_ValueEnd[1] - c_ValueEnd[0] + c_PriorityF = c_ValueEnd[2] - c_ValueEnd[1] + self.assertGreater(c_PriorityW, c_PriorityR) + self.assertGreater(c_PriorityW, c_PriorityF) + self.assertGreater(c_PriorityF, c_PriorityR) + +if __name__ == '__main__': + unittest.main(failfast=False) + diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..5935425 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,2 @@ +__all__ = ["RWLock"] +