mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-16 21:37:59 +08:00
* Improved fix for matrix cache handling. During the MatrixDataCacheItem acquire function there was a time when the write lock was released and the read lock was taken. During that time, the dataset could have been deleted, later result in the MatrixDataCacheManageri data adaptor returning None. The solution is to demote the writer lock to a reader lock instead of unlocking and relocking. Also, when a the cache needs to delete an entry, the delete is done outside the MatrixDataCacheManager lock. This operation only requires the write lock for the MatrixDataCacheItem. Fixes #1255
126 lines
3.4 KiB
Python
126 lines
3.4 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_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()
|