-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Introduce a fourth type of LazyThreadSafetyMode: ThreadSafeValueOnly #27421
Comments
Seems reasonable functionally, albeit with a better name 😄. Maybe |
Shall we flip this to ready-for-review and we can discuss naming there? |
This is very important to have. If a lazy instance is used as a cache in a web application and it's execution fails then the web app is permanently down. Many things worth caching can fail transiently (any database or web service call). The The weaker modes are not very nice to use in caching scenarios because they can cause cache stampeding due to multiple executions. |
Thinking about this more, I'd prefer a slightly different design. There's nothing in the public Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode, bool storeExceptions); |
There's two reasons I added a new enum value rather than add a boolean argument: First of I'm of the opinion that adding bools as arguments is bad for readability of the code, and that it is better to use enums or have multiple methods. This is well documented and is a pattern I've seen other places in C#, for example in Index Of, which could have taken a boolean for case invariance but instead uses an enum with several values. And second is that I don't think features should be added unless they are needed. Yes, the other options could use a flag for toggling caching of exceptions too, but is there a real demand for that? It would just make the already complex Lazy even more complex, maybe not much in code but definitely in understanding all the possible combinations and which one is the correct one for the situation at hand. |
I understand your reasons. But whether the exception is cached or not has little to do with the thread safety mode of the object, which is what that enum is about. From my perspective, adding an unrelated and orthogonal value to an enum is more confusing and more complex than adding it as a separate argument. In the IndexOf example, case is a part of how the string comparison happens, so it makes sense as part of something name "StringComparison"; that's not the case here. |
I see what you mean. It's a bit weird that two of the |
There wouldn't be a default value.
Thankfully we have named arguments now, e.g. new Lazy<T>(..., storeExceptions: true); |
'Default value' as in one of the overloads that don't take the bool argument. Most of the time when there is an overload without a bool argument it's clear how it will behave, as if there was a default value, but in this case it might be more difficult to document clearly. |
But I'm not objecting to your suggestion, so if there is a vote or something now (I've never proposed something here before) it has my vote 👍 |
If the example use case is caching the result of a web request, doesn't that mean adding this would promote waiting synchronously for IO operations? And I think the pattern of using |
Async support for |
@stephentoub, what is the status of this? |
Nothing has changed. It's still marked as api-suggestion, and the design was still being debated. |
Since nothing has changed here I have made a very simple implementation of a public class AtomicLazy<T>
{
private readonly Func<T> _factory;
private T _value;
private bool _initialized;
private object _lock;
public AtomicLazy(Func<T> factory)
{
_factory = factory;
}
public AtomicLazy(T value)
{
_value = value;
_initialized = true;
}
public T Value => LazyInitializer.EnsureInitialized(ref _value, ref _initialized, ref _lock, _factory);
} |
Any update on this? It would be pretty useful to have either a no cache exception mode, or at a least a property to check if the Lazy threw when created, right now IsValueCreated can mean either Lazy not initialized or Lazy tried to initialized but threw. Then you could have something like: if (lazy.ExceptionOnInit) |
@mariusGundersen this is a decent implementation, but I think that a more desirable behavior would be to propagate the exception to all the threads that are currently waiting. A thread should not have to wait for longer than the duration of a single I've posted an alternative implementation here ( |
Any updates here? Only the value factory constructor caches exceptions by default, this feels like a trap. It would be nice to at least have an option to not cache exceptions. |
With @stephentoub's comment, the proposal would be public class Lazy<T>
{
+ public Lazy (Func<T> valueFactory, System.Threading.LazyThreadSafetyMode mode, bool storeExceptions);
} I agree that the If we're okay with that proposal I could be interested implementing that change. |
Instead of the boolean we could also add a new enum public class Lazy<T>
{
+ public Lazy (Func<T> valueFactory, System.Threading.LazyThreadSafetyMode mode, System.LazyOptions options);
}
+
+ [System.Flags]
+ public enum LazyOptions
+ {
+ None = 0,
+ /// <summary>
+ /// Avoid catching and storing the exception the initialization function could throw when calling
+ /// <see cref="Lazy{T}.Value" />. In that case, the exception is thrown, nothing is stored, and the next call
+ /// to <see cref="Lazy{T}.Value" /> would call the initialization function again.
+ /// </summary>
+ /// <remarks>This option is redundant when the thread safe mode is <see cref="LazyThreadSafetyMode.PublicationOnly" />.</remarks>
+ ThrowInitializationExceptions = 1,
+ }
|
(This is opened based on feedback from a pull-request to coreclr)
This is a request to enhance the
LazyThreadSafetyMode
enum and theLazy<T>
class to support a fourth scenario.Rationale and Description
There seems to be a missing LazyThreadSafetyMode that is commonly asked for (for example), which would behave sort of as a mix of
ExecutionAndPublication
andPublishOnly
. This mode is similar toExecutionAndPublication
in that it uses locks so only a single thread can run the factory at a time, and other threads will wait until the first thread is done. But it is different in that it does not cache exceptions, only successful values, similar to whatPublicationOnly
does. If the value factory throws an exception then theLazy<T>
remains in the pending state, so that the next time theValue
is accessed it will call the factory again.Usecases
This is very useful when
Lazy<T>
is used for values that are expected to fail intermittently but recover quickly. For example, it might be used to store the result of a network request triggered by incoming requests to a web server. All of these need to be satisfied in this scenario:Lazy<T>
is used)There currently isn't any simple way to do this without having to rewrite
Lazy<T>
completely from scratch. Luckily it is not very difficult to extend it to support this scenarioPotential implementation
Having looked at the code it seems like the only change needed is to not store the exception when it is thrown, so that the next call finds the
Lazy<T>
in the initial pending state. The simplest way to changeLazy<T>
to behave this way is to remove this line. Obviously it shouldn't be changed, but enhanced, and so a new mode is needed that is identical to the code path of theExecutionAndPublication
mode, except for missing this single line. I have attempted to do this in the pull-request to coreclr to coreclr.Discussion
The name ThreadSafeValueOnly is very much bikeshedable, so if you have a suggestion for another name of this mode I am very much up for changing it.
The text was updated successfully, but these errors were encountered: