[issue43468] functools.cached_property incorrectly locks the entire descriptor on class instead of per-instance locking

Raymond Hettinger report at bugs.python.org
Wed Aug 4 01:19:26 EDT 2021


Raymond Hettinger <raymond.hettinger at gmail.com> added the comment:

Here's the latest effort:

---------------------------------------------------------------

    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        if self.attrname is None:
            raise TypeError(
                "Cannot use cached_property instance without calling __set_name__ on it.")
        try:
            cache = instance.__dict__
        except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
            msg = (
                f"No '__dict__' attribute on {type(instance).__name__!r} "
                f"instance to cache {self.attrname!r} property."
            )
            raise TypeError(msg) from None

        # Quickly and atomically determine which thread is reponsible
        # for doing the update, so other threads can wait for that
        # update to complete.  If the update is already done, don't
        # wait.  If the updating thread is reentrant, don't wait.
        key = id(self)
        this_thread = get_ident()
        with self.updater_lock:
            val = cache.get(self.attrname, _NOT_FOUND)
            if val is not _NOT_FOUND:
                return val
            wait = self.updater.setdefault(key, this_thread) != this_thread

        # ONLY if this instance currently being updated, block and wait
        # for the computed result. Other instances won't have to wait.
        # If an exception occurred, stop waiting.
        if wait:
            with self.cv:
                while cache.get(self.attrname, _NOT_FOUND) is _NOT_FOUND:
                    self.cv.wait()
                val = cache[self.attrname]
                if val is not _EXCEPTION_RAISED:
                    return val

        # Call the underlying function to compute the value.
        try:
            val = self.func(instance)
        except Exception:
            val = _EXCEPTION_RAISED

        # Attempt to store the value
        try:
            cache[self.attrname] = val
        except TypeError:
            # Note: we have no way to communicate this exception to
            # threads waiting on the condition variable.  However, the
            # inability to store an attribute is a programming problem
            # rather than a runtime problem -- this exception would
            # likely occur early in testing rather than being runtime
            # event triggered by specific data.
            msg = (
                f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                f"does not support item assignment for caching {self.attrname!r} property."
            )
            raise TypeError(msg) from None

        # Now that the value is stored, threads waiting on the condition
        # variable can be awakened and the updater dictionary can be
        # cleaned up.
        with self.updater_lock:
            self.updater.pop(key, None)
            cache[self.attrname] = _EXCEPTION_RAISED
            self.cv.notify_all()

        if val is _EXCEPTION_RAISED:
            raise
        return val

----------

_______________________________________
Python tracker <report at bugs.python.org>
<https://bugs.python.org/issue43468>
_______________________________________


More information about the Python-bugs-list mailing list