.NET 4.0&#39; System.Lazy<T>类通过枚举LazyThreadSafetyMode提供三种线程安全模式,我将其概括为:
我想要一个延迟初始化的值,它遵循稍微不同的线程安全规则,即:
只有一个并发线程会尝试创建基础值。成功创建后,所有等待的线程将获得相同的值。如果在创建过程中发生未处理的异常,它将在每个等待的线程上重新抛出,但它不会被缓存,随后尝试访问基础值将重新尝试创建&amp;可能会成功。
因此 LazyThreadSafetyMode.ExecutionAndPublication 的关键是,如果&#34;首先去&#34;在创建失败时,可以在以后重新尝试。
是否存在提供这些语义的现有(.NET 4.0)类,还是我必须自己滚动?如果我自己滚动是有一种聪明的方法来重新使用现有的Lazy&lt; T&gt;在实现中避免显式锁定/同步?
N.B。对于一个用例,想象一下&#34;创建&#34;可能是昂贵的并且容易出现间歇性错误,包括例如从远程服务器获取大量数据。我不想进行多次并发尝试来获取数据,因为他们可能都会失败或全部成功。但是,如果它们失败了,我希望以后能够重试。
答案 0 :(得分:4)
只有一个并发线程会尝试创建底层证券 值。成功创建后,所有等待的线程都将收到 相同的价值。如果在创建过程中发生未处理的异常,它将会 在每个等待的线程上重新抛出,但它不会被缓存 后续尝试访问基础值将重新尝试 创造与可能会成功。
由于Lazy不支持,你可以尝试自己滚动它:
private static object syncRoot = new object();
private static object value = null;
public static object Value
{
get
{
if (value == null)
{
lock (syncRoot)
{
if (value == null)
{
// Only one concurrent thread will attempt to create the underlying value.
// And if `GetTheValueFromSomewhere` throws an exception, then the value field
// will not be assigned to anything and later access
// to the Value property will retry. As far as the exception
// is concerned it will obviously be propagated
// to the consumer of the Value getter
value = GetTheValueFromSomewhere();
}
}
}
return value;
}
}
更新:
为了满足您对传播到所有等待读者线程的相同异常的要求:
private static Lazy<object> lazy = new Lazy<object>(GetTheValueFromSomewhere);
public static object Value
{
get
{
try
{
return lazy.Value;
}
catch
{
// We recreate the lazy field so that subsequent readers
// don't just get a cached exception but rather attempt
// to call the GetTheValueFromSomewhere() expensive method
// in order to calculate the value again
lazy = new Lazy<object>(GetTheValueFromSomewhere);
// Re-throw the exception so that all blocked reader threads
// will get this exact same exception thrown.
throw;
}
}
}
答案 1 :(得分:2)
懒惰不支持这个。这是Lazy的一个设计问题,因为异常“缓存”意味着该延迟实例不会永远提供真正的值。由于网络问题等瞬态错误,这可能会永久性地导致应用程序停机。那时通常需要人为干预。
我敢打赌,这个地雷存在于不少.NET应用程序中......
你需要写自己的懒惰才能做到这一点。或者,为此打开CoreFx Github问题。
答案 2 :(得分:1)
部分受Darin's answer的启发,但试图让这个“等待线程的队列被异常”和“再试一次”功能工作:
private static Task<object> _fetcher = null;
private static object _value = null;
public static object Value
{
get
{
if (_value != null) return _value;
//We're "locking" then
var tcs = new TaskCompletionSource<object>();
var tsk = Interlocked.CompareExchange(ref _fetcher, tcs.Task, null);
if (tsk == null) //We won the race to set up the task
{
try
{
var result = new object(); //Whatever the real, expensive operation is
tcs.SetResult(result);
_value = result;
return result;
}
catch (Exception ex)
{
Interlocked.Exchange(ref _fetcher, null); //We failed. Let someone else try again in the future
tcs.SetException(ex);
throw;
}
}
tsk.Wait(); //Someone else is doing the work
return tsk.Result;
}
}
我有点担心 - 有人能看到任何明显的比赛,它会以一种不明显的方式失败吗?
答案 3 :(得分:1)
我尝试的Darin's updated answer版本没有竞争条件我pointed out ...警告,我不完全确定这最终完全没有种族条件。
private static int waiters = 0;
private static volatile Lazy<object> lazy = new Lazy<object>(GetValueFromSomewhere);
public static object Value
{
get
{
Lazy<object> currLazy = lazy;
if (currLazy.IsValueCreated)
return currLazy.Value;
Interlocked.Increment(ref waiters);
try
{
return lazy.Value;
// just leave "waiters" at whatever it is... no harm in it.
}
catch
{
if (Interlocked.Decrement(ref waiters) == 0)
lazy = new Lazy<object>(GetValueFromSomewhere);
throw;
}
}
}
更新:我发现在发布此消息后我发现了一个竞争条件。这个行为实际上应该是可以接受的,只要你可以接受一个可能很少见的情况,其中一些线程抛出一个异常,它在另一个线程已经从一个成功的快速返回后从慢Lazy<T>
观察到{{1} (未来的请求都会成功)。
Lazy<T>
= 0 waiters
(Interlocked.Decrement
= 1)之前运行waiters
(Interlocked.Increment
= 1)之前waiters
并准备覆盖(Interlocked.Decrement
= 0)waiters
(Interlocked.Decrement
= 1)waiters
覆盖lazy
(称之为lazy1
)(waiters
= 1)lazy1
(waiters
= 2)Interlocked.Decrement
(waiters
= 1)lazy1
的值(waiters
现在无关紧要)我无法提出一系列事件,这些事件会导致比#34更糟糕的事情;这个线程在另一个线程产生成功结果后引发异常&#34;。
Update2:将lazy
声明为volatile
,以确保所有读者立即看到保护覆盖。有些人(包括我自己)看到volatile
并立即思考&#34;好吧,这可能被错误地使用了#34;并且他们通常是正确的。这就是我在这里使用它的原因:在上面示例的事件序列中,如果t3位于读取{{lazy
之前,它仍然可以读取旧lazy1
而不是lazy.Value
1}} t1修改lazy
以包含lazy1
的那一刻。 volatile
可以防止这种情况发生,以便下一次尝试可以立即开始。
我还提醒自己为什么我脑子里有这个东西说&#34;低锁并发编程很难,只需使用C#lock
语句!!!&# 34;我一直在写原始答案。
Update3:刚刚更改了Update2中的一些文本,指出了使volatile
成为必要的实际情况 - 这里使用的Interlocked
操作显然是在今天的重要CPU架构上全屏实现的,而不是我原来只是假装的半栅栏,所以volatile
保护了比我原先想象的更窄的部分。
答案 4 :(得分:1)
这样的事情可能有所帮助:
using System;
using System.Threading;
namespace ADifferentLazy
{
/// <summary>
/// Basically the same as Lazy with LazyThreadSafetyMode of ExecutionAndPublication, BUT exceptions are not cached
/// </summary>
public class LazyWithNoExceptionCaching<T>
{
private Func<T> valueFactory;
private T value = default(T);
private readonly object lockObject = new object();
private bool initialized = false;
private static readonly Func<T> ALREADY_INVOKED_SENTINEL = () => default(T);
public LazyWithNoExceptionCaching(Func<T> valueFactory)
{
this.valueFactory = valueFactory;
}
public bool IsValueCreated
{
get { return initialized; }
}
public T Value
{
get
{
//Mimic LazyInitializer.EnsureInitialized()'s double-checked locking, whilst allowing control flow to clear valueFactory on successful initialisation
if (Volatile.Read(ref initialized))
return value;
lock (lockObject)
{
if (Volatile.Read(ref initialized))
return value;
value = valueFactory();
Volatile.Write(ref initialized, true);
}
valueFactory = ALREADY_INVOKED_SENTINEL;
return value;
}
}
}
}