Last active
January 15, 2025 00:16
-
-
Save tylerneylon/a7ff6017b7a1f9a506cf75aa23eacfd6 to your computer and use it in GitHub Desktop.
A simple read-write lock implementation in Python.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
""" rwlock.py | |
A class to implement read-write locks on top of the standard threading | |
library. | |
This is implemented with two mutexes (threading.Lock instances) as per this | |
wikipedia pseudocode: | |
https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock#Using_two_mutexes | |
Code written by Tyler Neylon at Unbox Research. | |
This file is public domain. | |
""" | |
# _______________________________________________________________________ | |
# Imports | |
from contextlib import contextmanager | |
from threading import Lock | |
# _______________________________________________________________________ | |
# Class | |
class RWLock(object): | |
""" RWLock class; this is meant to allow an object to be read from by | |
multiple threads, but only written to by a single thread at a time. See: | |
https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock | |
Usage: | |
from rwlock import RWLock | |
my_obj_rwlock = RWLock() | |
# When reading from my_obj: | |
with my_obj_rwlock.r_locked(): | |
do_read_only_things_with(my_obj) | |
# When writing to my_obj: | |
with my_obj_rwlock.w_locked(): | |
mutate(my_obj) | |
""" | |
def __init__(self): | |
self.w_lock = Lock() | |
self.num_r_lock = Lock() | |
self.num_r = 0 | |
# ___________________________________________________________________ | |
# Reading methods. | |
def r_acquire(self): | |
self.num_r_lock.acquire() | |
self.num_r += 1 | |
if self.num_r == 1: | |
self.w_lock.acquire() | |
self.num_r_lock.release() | |
def r_release(self): | |
assert self.num_r > 0 | |
self.num_r_lock.acquire() | |
self.num_r -= 1 | |
if self.num_r == 0: | |
self.w_lock.release() | |
self.num_r_lock.release() | |
@contextmanager | |
def r_locked(self): | |
""" This method is designed to be used via the `with` statement. """ | |
try: | |
self.r_acquire() | |
yield | |
finally: | |
self.r_release() | |
# ___________________________________________________________________ | |
# Writing methods. | |
def w_acquire(self): | |
self.w_lock.acquire() | |
def w_release(self): | |
self.w_lock.release() | |
@contextmanager | |
def w_locked(self): | |
""" This method is designed to be used via the `with` statement. """ | |
try: | |
self.w_acquire() | |
yield | |
finally: | |
self.w_release() |
use time.sleep(1) to give them a chance to gain the lock
import threading
import time
# from rwlock import RWLock
marker = RWLock()
WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
today = 0
def calendar_reader(id_number):
global today
name = 'Reader-' + str(id_number)
while today < len(WEEKDAYS)-1:
# print(today)
with marker.r_locked():
print(name, 'sees that today is', WEEKDAYS[today])
time.sleep(1)
def calendar_writer(id_number):
global today
name = 'Writer-' + str(id_number)
while today < len(WEEKDAYS)-1:
with marker.w_locked():
today = (today + 1) % 7
print(name, 'updated date to ', WEEKDAYS[today])
time.sleep(1)
if __name__ == '__main__':
for i in range(10):
threading.Thread(target=calendar_reader, args=(i,)).start()
for i in range(2):
threading.Thread(target=calendar_writer, args=(i,)).start()
result:
Reader-0 sees that today is Sunday
Reader-1 sees that today is Sunday
Reader-2 sees that today is Sunday
Reader-3 sees that today is Sunday
Reader-4 sees that today is Sunday
Reader-5 sees that today is Sunday
Reader-6 sees that today is Sunday
Reader-7 sees that today is Sunday
Reader-8 sees that today is Sunday
Reader-9 sees that today is Sunday
Writer-0 updated date to Monday
Writer-1 updated date to Tuesday
Reader-4 sees that today is Tuesday
Reader-6 sees that today is Tuesday
Reader-0 sees that today is Tuesday
Reader-7 sees that today is Tuesday
Reader-3 sees that today is Tuesday
Reader-5 sees that today is Tuesday
Reader-2 sees that today is Tuesday
Reader-8 sees that today is Tuesday
Reader-1 sees that today is Tuesday
Reader-9 sees that today is Tuesday
Writer-0 updated date to Wednesday
Writer-1 updated date to Thursday
Reader-9 sees that today is Thursday
Reader-4 sees that today is Thursday
Reader-2 sees that today is Thursday
Reader-7 sees that today is Thursday
Reader-1 sees that today is Thursday
Reader-0 sees that today is Thursday
Reader-5 sees that today is Thursday
Reader-6 sees that today is Thursday
Reader-3 sees that today is Thursday
Reader-8 sees that today is Thursday
Writer-1 updated date to Friday
Writer-0 updated date to Saturday
The advantage of a queueless lock is that readers don't have to wait for each other. The advantage of a queued lock is that writers won't be starved.
@tylerneylon, If you put queue on r_acquire
only (not the whole operation), readers will not block each other. This is called "fair" order.
Here is example with more detailed explanation https://en.wikipedia.org/wiki/Readers%e2%80%93writers_problem#Third_readers%E2%80%93writers_problem.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I believe my implementation does not have this problem I have a list of thread ids that are reading, and my release method will not do anything if the current thread did not call acquire_read before
in my implementation i used a condition lock for the
read_ready_state
any writer or reader will need to acquire the state before they can do read or write requests, so there will be a queue which is managed by the languge and will give the lock by the order of callers. so when the writer requests the state other read requests will have to be after when the writer has released the state