Skip to content

Instantly share code, notes, and snippets.

@tylerneylon
Last active May 2, 2024 12:46
Show Gist options
  • Star 29 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save tylerneylon/a7ff6017b7a1f9a506cf75aa23eacfd6 to your computer and use it in GitHub Desktop.
Save tylerneylon/a7ff6017b7a1f9a506cf75aa23eacfd6 to your computer and use it in GitHub Desktop.
A simple read-write lock implementation in Python.
# -*- 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()
@Eboubaker
Copy link

A forked version with support for with blocks https://gist.github.com/Eboubaker/6a0b807788088a764b2a4c156fda0e4b

@tylerneylon
Copy link
Author

Hi @Eboubaker ! As a friendly fyi, this gist does include support for with blocks (this is done via contextmanager).

Example usage:

from rwlock import RWLock

rwlock = RWLock()  # This is a lock for my_obj.

# When reading from my_obj:
with rwlock.r_locked():
    do_read_only_things_with(my_obj)

# When writing to my_obj:
with rwlock.w_locked():
    mutate(my_obj)

@Eboubaker
Copy link

Eboubaker commented Oct 29, 2022

def update_things():
   with rwlock.w_locked():
    #  update state

with rwlock.r_locked():
    # read state
    if some_confition:
       update_things()

this example will hang in your implementation if the same thread was doing the work,
you have to move lock from update_things, but this method is being called from multiple places, and you have to keep adding with rwlock.w_locked(): before each call to the method, code will be less readable

@tylerneylon
Copy link
Author

Thanks for explaining. You've designed your lock to be reentrant, which is cool. If I have time I may add this feature here, but for now it is not a reentrant lock.

@Eboubaker
Copy link

Eboubaker commented Oct 29, 2022

@tylerneylon, I think your implementation is based on the assumption that a mutex acquired by one thread can be released by another. Please look at the first implementation in the below link, it is similar to yours: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock

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

@Nibbetts So if I understand correctly, as long as any readers are reading, more can be added on even if there is a writer waiting. Perhaps you could avoid this potentially infinite wait by having w_acquire acquire num_r_lock, and w_release release it?

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

@littlefattiger
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment