> Thread 2 wakes up with the lock, calls the function, fills the cache, and 
> releases the lock.


What exactly would the issue be with this:

```
import functools
from threading import Lock

def once(func):
    sentinel = object()
    cache = sentinel
    lock = Lock()

    @functools.wraps(func)
    def _wrapper():
        nonlocal cache, lock, sentinel
        if cache is sentinel:
            with lock:
                if cache is sentinel:
                    cache = func()
        return cache

    return _wrapper
```

It ensures it’s invoked once, it’s fast and it doesn’t require acquiring a lock 
needlessly. Always wrapping a lock around a static value to account for a small 
edge case seems redundant, more than “it’s usually not optimal”. It’s never 
optimal: in multi-threaded cases you’re causing needless contention, and in 
single threaded cases all your calls are now slower.

Seems generally more correct, even in single threaded cases, to pay the 
overhead only in the first call if you want `call_once` semantics. Which is why 
you would be using `call_once` in the first place?


> On 29 Apr 2020, at 18:51, Andrew Barnert <abarn...@yahoo.com> wrote:
> 
> On Apr 29, 2020, at 00:34, Tom Forbes <t...@tomforb.es> wrote:
>> 
>> It’s not quite that easy, either you needlessly lock all calls or you end 
>> up invoking it twice.
>> 
>> What you want is to acquire a lock if the cache is empty, check if another 
>> thread has filled the cache while you where waiting on the lock, call the 
>> function, fill the cache and return.
> 
> Thread 1 sees the cache is empty, so it acquires the lock, runs the function. 
> 
> Thread 2 sees the cache is empty, so it tries to acquire the cache and blocks.
> 
> Thread 1 finishes the function, fills the cache, and releases the lock.
> 
> Thread 2 wakes up with the lock, calls the function, fills the cache, and 
> releases the lock.
> 
> You’ve now run the function twice instead of once, and the first caller got a 
> different value than the one every subsequent caller can get.
> 
> There are actually plenty of cases where this is acceptable (the function is 
> safe, and idempotent, and while it’s slow it’s not so slow that occasionally 
> running it twice is worse than locking a zillion times)—but in those cases, 
> just not using a lock is even better. In fact, if you can compare-and-swap at 
> the end (which requires a lock to simulate in Python, but it’s a much more 
> granular one than locking over the whole expensive function call), you can 
> even use it in more cases (in particular, where the function isn’t idempotent 
> but you need to value to be).
> 
> This is exactly why users should be encouraged to just lock the whole thing. 
> It’s always correct. It’s usually not optimal, but often good enough. The one 
> really common and simple case where it’s not good enough is single-threaded 
> code. Anything else where you need to optimize, you really do need to 
> understand the pattern you’re trying to optimize for (and the environment(s) 
> you’re running under) and write your own function.
> 
> 
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/MHTUCWAKNA72ACADM6N6VNCYRM3IBY7T/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to