Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lazy_static without locks? #116

Open
pganssle opened this issue Aug 17, 2018 · 3 comments
Open

lazy_static without locks? #116

pganssle opened this issue Aug 17, 2018 · 3 comments

Comments

@pganssle
Copy link

pganssle commented Aug 17, 2018

I have been working on a wrapper of Python's datetime library, which has a one-time initialization of an API object. I felt that lazy_static gave the best semantics, but it turns out it can cause deadlocks when used in multiple threads if the stuff inside the lazy_static acquires locks.

In my example, I have:

lazy_static! {
    pub static ref PyDateTimeAPI: PyDateTime_CAPI = unsafe { PyDateTime_IMPORT() };
}

The PyDateTimeAPI is only accessed within functions that have acquired the global interpreter lock (GIL), but PyDateTime_IMPORT() releases and then re-acquires the GIL. This causes a deadlock as such:

  • Thread 1 acquires GIL
  • Thread 2 blocks on GIL
  • Thread 1 acquires lazy_static lock
  • Thread 1 calls import, which releases the GIL
  • Thread 2 acquires GIL
  • Thread 2 blocks on lazy_static lock
  • Thread 1 completes import call and blocks on GIL

In this case, I happen to know that the code I'm calling is already thread-safe, and the value of PyDateTime_IMPORT() is guaranteed by contract to return the same object on a subsequent call.

Is it possible to get a version of lazy_static that does not attempt to acquire a lock? Having it be unsafe is also fine.

Another option might be that the no-lock version of lazy_static doesn't lock around the function body, but does lock around assigning the value. In pseudocode it would be something like this:

let mut const_val : Option<T> = None;
fn get_const_val<T, F>(f: F) -> &T {
    match const_val {
        Some(v) => v,
        None => {
                let v = f();    // No lock, this may be called many times
                global_lock.acquire();
                // If f() was called maybe once and we lost the race,
                // the previous invocation will have set const_val
                let rv = match const_val {
                    Some(cv) => cv,
                    None => {
                        const_val = v;
                        v
                    }
                };
                global_lock.release();
                rv
        }
    }
}

So long as the function body has no side effects, the worst case scenario is duplicated effort.

I'd be fine if the macro were only available in unsafe blocks.

@KodrAus
Copy link
Contributor

KodrAus commented Sep 26, 2018

Hi @pganssle, hmm this is pretty unfortunate, but possibly a bit out-of-scope for lazy_static to support. I think the semantic differences are a bit too subtle for us to support both approaches, and there are equal drawbacks to duplicated effort where other side-effects are involved in the initialization.

Have you already worked around this in your code?

@pganssle
Copy link
Author

@KodrAus Yes, I took effectively the same approach lazy_static does, implementing Deref on a "dummy struct", but locking around the assignment rather than the execution of the block that returns the value I want. See here.

I totally understand if it's out of scope, it's a pretty unusual situation.

@KodrAus
Copy link
Contributor

KodrAus commented Sep 27, 2018

@pganssle Glad to hear you found your way around it 👍

Something I think we could do better is describe how values are initialized in the docs better. We have this section on implementation details, but it's pretty light in details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants