不能直接用Lazy实现异步延迟初始化,因其仅懒加载Task对象而不控制执行时机;AsyncLazy通过Lazy确保首次访问才执行且线程安全,工厂函数须返回未启动的Task,禁止在其中await。

为什么不能直接用 Lazy>
很多人第一反应是套一层 Lazy,比如:
var lazyTask = new Lazy这看似异步延迟初始化,但问题在于:它只懒加载 任务对象本身,不控制任务的 执行时机。一旦你调用>(() => FetchDataAsync());
lazyTask.Value,它就立刻返回一个已启动(可能已完成、正在运行或已失败)的 Task,无法保证「首次访问时才真正开始执行」——尤其当多个线程并发访问时,FetchDataAsync() 可能被多次触发,违背 lazy 语义。
AsyncLazy 必须确保首次访问才执行且线程安全
核心诉求是:第一次调用 .Value 或 .Wait() / await 时,才真正启动异步工作,并且所有后续并发访问都复用同一个 Task,不重复执行。推荐做法是内部封装一个 Task 字段 + object 锁 + 双重检查(或直接用 Lazy 配合惰性构造函数)。最简健壮实现如下:
public class AsyncLazy{ private readonly Lazy > _lazy; public AsyncLazy(Func > factory) { _lazy = new Lazy >(() => factory()); } public Task Value => _lazy.Value; public async Task GetValueAsync() => await Value; }
关键点:
-
_lazy是Lazy,其> Value属性保证只执行一次工厂函数 - 工厂函数
Func返回的是未启动的> Task(如Task.Run(...)或HttpClient.GetStringAsync(...)),.NET 的Task构造即启动,所以必须确保工厂函数每次返回的是“新创建的、尚未 await 的 task” - 不要在工厂里
await—— 否则Lazy.Value会同步阻塞,失去异步意义
如何正确使用 AsyncLazy 并避免常见错误
错误示范:
var bad = new AsyncLazy正确写法是把(() => { var result = await GetDataFromApi(); // ❌ 编译不过:lambda 不能是 async return result; });
async 提到外部,用 Task.Run 或直接返回可等待的异步方法调用(前提是该方法返回 Task):
- ✅ 直接传异步方法组:
new AsyncLazy(GetDataFromApi) - ✅ 匿名函数中不 await,只返回 task:
() => GetDataFromApi() - ✅ 若需组合逻辑,用
Task.Run(注意:仅限 CPU-bound 场景):() => Task.Run(() => ExpensiveCalculation()).ContinueWith(t => t.Result.ToString()) - ❌ 不要用
Task.FromResult包装同步结果再声称是 AsyncLazy —— 它失去了异步延迟的意义
调用时也注意:如果只是想等结果,用 await lazy.Value;如果需要同步阻塞(极少见),用 lazy.Value.GetAwaiter().GetResult(),而非 .Value.Result(可能死锁)。
要不要支持取消和异常缓存?
标准 AsyncLazy 不内置取消令牌,因为 Lazy 本身也不支持。如需 CancellationToken,必须扩展构造函数并把 token 传入工厂,例如:
public AsyncLazy(Func但要注意:如果工厂抛出异常,> factory, CancellationToken cancellationToken = default) { _lazy = new Lazy >(() => factory(cancellationToken)); }
Lazy> 会缓存该异常 task,后续访问仍抛相同异常 —— 这是预期行为(类似 Lazy 缓存异常)。若需重试,就得自己封装 retry 逻辑,不在 AsyncLazy 职责范围内。
真正容易被忽略的是:AsyncLazy 的生命周期管理。它持有的 Task 不会自动释放,如果工厂返回的是长时运行或资源密集型 task(如未关闭的 HttpClient 请求),要确保上层控制好它的存活时间,必要时用 IDisposable 包装或依赖注入作用域来约束。










