[Twisted-Python] Waiting for a contended resource
Richard van der Hoff
richard at matrix.org
Mon Mar 12 16:56:42 MDT 2018
Thank you for all for all the answers so far, particularly to Ilya and
Jean-Paul who provided some very helpful code samples.
It's interesting to realise that, by avoiding locking, we can end up
with a much more efficient implementation. I'll have to figure out how
widely we can apply this technique - and how often it's going to be
worth rewriting things to allow that. Thanks for some useful pointers!
Richard
On 12/03/18 20:00, Jean-Paul Calderone wrote:
> On Mon, Mar 12, 2018 at 3:52 PM, Ilya Skriblovsky
> <ilyaskriblovsky at gmail.com <mailto:ilyaskriblovsky at gmail.com>> wrote:
>
> Hi, Richard,
>
> I've used class like this to cache the result of Expensive
> Calculation:
>
> class DeferredCache:
> pending = None
> result = None
> failure = None
>
> def __init__(self, expensive_func):
> self.expensive_func = expensive_func
>
> def __call__(self):
> if self.pending is None:
> def on_ready(result):
> self.result = result
> def on_fail(failure):
> self.failure = failure
>
> self.pending =
> defer.maybeDeferred(self.expensive_func).addCallbacks(on_ready,
> on_fail)
>
> return self.pending.addCallback(self._return_result)
>
>
> This seems like basically a correct answer to me. However, I suggest
> one small change.
>
> You probably want to create and return a new Deferred for each
> result. If you don't, then your internal `pending` Deferred is now
> reachable by application code.
>
> As written, an application might (very, very reasonably):
>
> d = getResource()
> d.addCallback(long_async_operation)
>
> Now `pending` has `long_async_operation` as a callback on its chain.
> This will prevent anyone else from getting a result until
> `long_async_operation` is done.
>
> You can fix this by:
>
> result = Deferred()
> self.pending.addCallback(self._return_result).chainDeferred(result)
> return result
>
> Now the application can only reach `result`. Nothing they do to
> `result` will make much difference to `pending` because
> `chainDeferred` only puts `callback` (and `errback`) onto `pending`'s
> callback chain. `callback` and `errback` don't wait on anything.
>
> You have to be a little careful with `chainDeferred` because it
> doesn't have the recursion-avoidance logic that implicit chaining
> has. However, that doesn't matter in this particular case because the
> chain depth is fixed at two (`pending` and `result`). The problems
> only arise if you extend the chain out in this direction without bound.
>
> Jean-Paul
>
> def _return_result(self, _):
> return self.failure or self.result
>
> Using it you can get rid of DeferredLocks:
>
> deferred_cache = DeferredCache(do_expensive_calculation)
>
> def getResource():
> return deferred_cache()
>
> It will start `expensive_func` on the first call. The second and
> consequtive calls will return deferreds that resolves with the
> result when expensive_func is done. If you call it when result is
> already here, it will return alread-fired deferred.
>
> Of course, it will require some more work if you need to pass
> arguments to `expensive_func` and memoize results per arguments
> values.
>
> -- ilya
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: </pipermail/twisted-python/attachments/20180312/7d6bca68/attachment-0002.html>
More information about the Twisted-Python
mailing list