From df46e7da061f0514101dd725217974fa0bfe7011 Mon Sep 17 00:00:00 2001 From: George Sakkis Date: Sun, 19 Apr 2015 23:57:59 +0300 Subject: [PATCH] Make cached_property thread-safe and alias threaded_cached_property to it. dict.setdefault() is atomic since three years ago; see http://bugs.python.org/issue13521 --- cached_property.py | 25 +----- tests/test_cached_property.py | 26 +----- tests/test_threaded_cached_property.py | 112 ------------------------- 3 files changed, 7 insertions(+), 156 deletions(-) delete mode 100644 tests/test_threaded_cached_property.py diff --git a/cached_property.py b/cached_property.py index 25ffe47..8e07fde 100644 --- a/cached_property.py +++ b/cached_property.py @@ -23,30 +23,11 @@ class cached_property(object): def __get__(self, obj, cls): if obj is None: return self - value = obj.__dict__[self.func.__name__] = self.func(obj) - return value - + return obj.__dict__.setdefault(self.func.__name__, self.func(obj)) -class threaded_cached_property(cached_property): - """ - A cached_property version for use in environments where multiple threads - might concurrently try to access the property. - """ - def __init__(self, func): - super(threaded_cached_property, self).__init__(func) - self.lock = threading.RLock() - - def __get__(self, obj, cls): - with self.lock: - # Double check if the value was computed before the lock was - # acquired. - prop_name = self.func.__name__ - if prop_name in obj.__dict__: - return obj.__dict__[prop_name] - - # If not, do the calculation and release the lock. - return super(threaded_cached_property, self).__get__(obj, cls) +# Leave for backwards compatibility +threaded_cached_property = cached_property class cached_property_with_ttl(object): diff --git a/tests/test_cached_property.py b/tests/test_cached_property.py index 40a5ad9..56678cf 100644 --- a/tests/test_cached_property.py +++ b/tests/test_cached_property.py @@ -1,11 +1,6 @@ # -*- coding: utf-8 -*- -""" -tests.py ----------------------------------- - -Tests for `cached-property` module. -""" +"""Tests for cached_property""" from time import sleep from threading import Lock, Thread @@ -88,10 +83,7 @@ class TestCachedProperty(unittest.TestCase): self.assertEqual(c.add_cached, None) def test_threads(self): - """ - How well does the standard cached_property implementation work with - threads? It doesn't, use threaded_cached_property instead! - """ + """How well does this implementation work with threads?""" class Check(object): @@ -109,8 +101,7 @@ class TestCachedProperty(unittest.TestCase): c = Check() threads = [] - num_threads = 10 - for x in range(num_threads): + for x in range(10): thread = Thread(target=lambda: c.add_cached) thread.start() threads.append(thread) @@ -118,13 +109,4 @@ class TestCachedProperty(unittest.TestCase): for thread in threads: thread.join() - # Threads means that caching is bypassed. - self.assertNotEqual(c.add_cached, 1) - - # This assertion hinges on the fact the system executing the test can - # spawn and start running num_threads threads within the sleep period - # (defined in the Check class as 1 second). If num_threads were to be - # massively increased (try 10000), the actual value returned would be - # between 1 and num_threads, depending on thread scheduling and - # preemption. - self.assertEqual(c.add_cached, num_threads) + self.assertEqual(c.add_cached, 1) diff --git a/tests/test_threaded_cached_property.py b/tests/test_threaded_cached_property.py deleted file mode 100644 index aa4898c..0000000 --- a/tests/test_threaded_cached_property.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -test_threaded_cache_property.py ----------------------------------- -Tests for `cached-property` module, threaded_cache_property. -""" - -from time import sleep -from threading import Thread, Lock -import unittest - -from cached_property import threaded_cached_property - - -class TestThreadedCachedProperty(unittest.TestCase): - - def test_cached_property(self): - - class Check(object): - - def __init__(self): - self.total1 = 0 - self.total2 = 0 - - @property - def add_control(self): - self.total1 += 1 - return self.total1 - - @threaded_cached_property - def add_cached(self): - self.total2 += 1 - return self.total2 - - c = Check() - - # The control shows that we can continue to add 1. - self.assertEqual(c.add_control, 1) - self.assertEqual(c.add_control, 2) - - # The cached version demonstrates how nothing new is added - self.assertEqual(c.add_cached, 1) - self.assertEqual(c.add_cached, 1) - - def test_reset_cached_property(self): - - class Check(object): - - def __init__(self): - self.total = 0 - - @threaded_cached_property - def add_cached(self): - self.total += 1 - return self.total - - c = Check() - - # Run standard cache assertion - self.assertEqual(c.add_cached, 1) - self.assertEqual(c.add_cached, 1) - - # Reset the cache. - del c.add_cached - self.assertEqual(c.add_cached, 2) - self.assertEqual(c.add_cached, 2) - - def test_none_cached_property(self): - - class Check(object): - - def __init__(self): - self.total = None - - @threaded_cached_property - def add_cached(self): - return self.total - - c = Check() - - # Run standard cache assertion - self.assertEqual(c.add_cached, None) - - def test_threads(self): - """ How well does this implementation work with threads?""" - - class Check(object): - - def __init__(self): - self.total = 0 - self.lock = Lock() - - @threaded_cached_property - def add_cached(self): - sleep(1) - # Need to guard this since += isn't atomic. - with self.lock: - self.total += 1 - return self.total - - c = Check() - threads = [] - for x in range(10): - thread = Thread(target=lambda: c.add_cached) - thread.start() - threads.append(thread) - - for thread in threads: - thread.join() - - self.assertEqual(c.add_cached, 1) -- 2.30.2