Files
cellxgene/server/data_common/rwlock.py
bmccandless 308ee64f30 Improvements to the matrix cache (#1340)
* Improvements to the matrix cache

- Add a timelimit for the matrix in the cache.
Once the timelimit is reached, the matrix can be removed.

- If a DatasetAccessError occurs, then remove the dataset
from the matrix cache.

Fixes #1322
2020-04-02 13:44:11 -07:00

136 lines
3.8 KiB
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.
Modified to add a w_demote function to convert a writer lock to a reader lock
"""
# _______________________________________________________________________
# 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
# The d_lock is needed to handle the demotion case,
# so that the writer can become a reader without releasing the w_lock.
# the d_lock is held by the writer, and prevents any other thread from taking the
# num_r_lock during that time, which means the writer thread is able to take the
# num_r_lock to update the num_r.
self.d_lock = Lock()
# ___________________________________________________________________
# Reading methods.
def r_acquire(self):
self.d_lock.acquire()
self.num_r_lock.acquire()
self.num_r += 1
if self.num_r == 1:
self.w_lock.acquire()
self.num_r_lock.release()
self.d_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.d_lock.acquire()
self.w_lock.acquire()
def w_acquire_non_blocking(self):
# if d_lock and w_lock can be acquired without blocking, acquire and return True,
# else immediately return False.
if self.d_lock.acquire(blocking=False):
if self.w_lock.acquire(blocking=False):
return True
else:
self.d_lock.release()
return False
def w_release(self):
self.w_lock.release()
self.d_lock.release()
def w_demote(self):
"""demote a writer lock to a reader lock"""
# the d_lock is already held from w_acquire.
# releasing the d_lock at the end of this function allows multiple readers.
# incrementing num_r makes this thread one of those readers.
self.num_r_lock.acquire()
self.num_r += 1
self.num_r_lock.release()
self.d_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()