目录
在软件开发领域,性能和效率是关键。作为开发人员,我们不断努力编写代码,不仅要解决手头的问题,而且要以最有效的方式解决问题。C#中的一种效率技术是使用Lazy<T>类进行延迟初始化。这使我们能够将可能较慢的初始化推迟到执行中的某个点,以便在我们需要使用值时更及时。但是,如果我们能更进一步,将异步引入其中呢?有什么东西可以给我们提供异步延迟功能吗?
在这篇博文中,我们将探讨C#中异步延迟初始化的概念,Lazy<Task<T>>用于实现这一点。因此,无论您是经验丰富的C#程序员还是好奇的初学者,请系好安全带,开启一段激动人心的异步延迟初始化世界之旅!与往常一样,我们将讨论我们今天所看到的内容的利弊。
了解延迟初始化
在我们深入研究异步世界之前,让我们先了解一下什么是延迟初始化。延迟初始化是一种编程技术,其中对象或值的初始化被推迟到首次访问它。当创建对象的成本很高(在时间或资源方面),并且应用程序启动时不会立即使用该对象时,这可能特别有用。如果没有延迟初始化,启动时间可能会不必要地失控。如果您一直在查看我关于插件架构的内容或使用Autofac进行依赖注入,您可能已经发现您的初始化代码范围正在扩大。
在C#中,这是使用Lazy<T>类实现的。下面是一个简单的示例:
Lazy<MyClass> myObject = new Lazy<MyClass>(() => new());
在上面的代码片段中,myObject是一个Lazy<MyClass>实例。在myObject.Value首次访问之前,不会创建实际MyClass对象。如果创建此MyClass属性的成本很高,并且您仅在第一次需要此属性之前访问它,则可以显著提高应用程序的启动性能。更重要的是,该Lazy<T>类型在初始化时处理线程安全!
异步延迟初始化的必要性
虽然Lazy<T>是一个强大的工具,但它似乎不支持异步初始化。当对象的初始化涉及I/O操作或其他长时间运行的任务时,这可能是一个问题,这些任务将受益于异步运行。阻止此类操作的主线程可能会导致用户体验不佳,因为它可能会使应用程序无响应。此外,随着async/await模式在.NET代码库中变得越来越普遍,异步延迟初始化的愿望将继续增长。
这就是Lazy<Task<T>>发挥作用的地方。通过使用Lazy<Task<T>>,我们可以实现异步延迟初始化。Task<T>表示返回结果的异步操作。当与Lazy<T>结合使用时,它允许异步运行成本高昂的操作,并在需要时使用其结果。
那么Lazy<T>是否支持开箱即用的异步延迟初始化?从技术上讲,是的,但是我们中的许多人从未考虑过我们可以将此处的类型参数替换为Task<T>!通过使用任务作为延迟包装器的类型,我们基本上可以开箱即用地实现异步延迟!许多C#开发人员已经意识到了这一点,但如果你像我一样,答案就藏在我们的眼皮底下。
Lazy<Task<T>>简介
让我们看看如何使用Lazy<Task<T>>实现异步延迟初始化。下面是一个简单的示例:
Lazy<Task<MyClass>> myObject = new Lazy<Task<MyClass>>(() => Task.Run(() => new MyClass()));
在上面的代码片段中,myObject是一个Lazy<Task<MyClass>>实例。Task.Run(() => new MyClass())是创建新MyClass对象的异步操作。在首次访问myObject.Value之前,此操作不会运行。此外,由于它被包装在Task中,因此它将异步运行。
而且,您不仅可以直接实例化对象,还可以做更多的事情!让我们看一下这个例子:
public async Task<MyClass> CreateMyClassAsync()
{
// simulate being busy!
await Task.Delay(2000);
return new MyClass();
}
Lazy<Task<MyClass>> myObject = new Lazy<Task<MyClass>>(CreateMyClassAsync);
上面的代码引用了一个async/await方法,该方法旨在证明从技术上讲,您可以传入任何异步代码路径。第一个示例只是显示一个对象的构造函数被调用,这有点做作,因为理想情况下,这应该几乎是即时的。因此,通过这个例子,希望你能开始看到运行时间更长的操作的潜力。
消费Lazy<Task<T>>
要消费Lazy<Task<T>>的结果,我们需要等待Task<T>:
MyClass result = await myObject.Value;
在上面的代码片段中,myObject.Value返回Task<MyClass>。通过等待此任务,我们可以在MyClass对象准备就绪后获取它。如果任务尚未完成,这将异步等待任务完成,然后再继续。这可确保应用程序保持响应,即使MyClass初始化需要很长时间。当然,这是假设代码中async/await模式的其余部分实际上已正确完成!
异步延迟初始化的强大功能
异步延迟初始化可以成为C#开发工具包中的强大工具(如果尚未使用Lazy<T>这种方式)。它结合了延迟初始化和异步编程的优点,允许您按需创建昂贵的对象,而不会阻塞主线程。同样,假设您正确使用async/await和任务!
以下是使用异步延迟初始化的一些好处:
- 改进的性能:通过将昂贵对象的创建推迟到需要时,可以提高应用程序的启动性能。
- 更好的响应能力:通过异步运行初始化代码,可以确保应用程序保持响应,即使初始化需要很长时间。
- 简单性:该Lazy<Task<T>>模式简单易用。它利用.NET中的现有Lazy<T>和Task<T>类,因此无需学习新的API。
- 控制:您可以控制要在何处支付初始化成本的代价。如果在初始化时声明变量是有意义的,但此时不为它们付出代价,那么您现在有一个工具可以做到这一点。
异步延迟注意事项
但是,像任何工具一样,它应该有目的地使用!过度使用延迟初始化(无论是同步还是异步)可能会导致其自身的问题,例如内存使用量增加,如果处理不当,可能会出现死锁。请记住,如果您要解决的问题是某些操作需要很长时间,Lazy<T>实际上并不能从本质上解决这个问题,它只是允许您移动它。如果你能把它移到一个有利的地方,那就太好了!但仅仅移动它并不一定能解决您的问题。
当我们混合其中的异步部分时,又增加了一层复杂性,而复杂性与延迟初始化不能很好地混合!考虑一下,如果你首先需要使用Lazy<Task<T>>,你可能有一些async/await代码要调用,或者其他一些任务需要运行。如果需要与其他系统交互(即从磁盘读取文件、从数据库提取数据、从Web API查询结果等),则可能会发生错误。运行此异步代码时,需要交互的复杂性和内容越多,出错的空间就越大。
您将需要考虑如果您正在运行Lazy<Task<T>>的代码可能会失败,它会是什么样子。在预期运行一次以为您缓存结果的事物中具有复原能力是什么样子的?我在这里不是要开出一个放之四海而皆准的解决方案,但我确实认为你需要认真考虑一下。在本文中,提供了一个可以允许一些容错的示例,但作者还指出,这可能看起来因场景而异。
Stephen Toub的AsyncLazy<T>
来自Microsoft的合作伙伴软件工程师Stephen Toub 提出了一个AsyncLazy<T>类,它结合了两全其美的优点:Lazy<T>的延迟和Task<T>异步。这是它的样子(代码与原始代码略有修改,因为博客文章已经有几年了):
public class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<T> valueFactory) :
base(() => Task.Run(valueFactory))
{ }
public AsyncLazy(Func<Task<T>> taskFactory) :
base(() => Task.Run(() => taskFactory()).Unwrap())
{ }
}
在此代码中,AsyncLazy<T>是Lazy<Task<T>>的子类。它提供了两个构造函数:一个采用Func<T>,另一个采用Func<Task<T>>。为什么我们仍然在第二个构造函数上使用Task.Run?好吧,直接从斯蒂芬那里看到这一点:
[如果我们没有将其包装在Task.Run中]这意味着当用户访问此实例的Value属性时,将同步调用taskFactory委托。如果taskFactory委托在返回任务实例之前只做很少的工作,这可能是完全合理的。但是,如果taskFactory委托执行任何不可忽略的工作,则Value调用将阻止,直到taskFactory调用完成。为了涵盖这种情况,第二种方法是使用[Task.Run]运行taskFactory,即异步运行委托本身,就像第一个构造函数一样,即使这个委托已经返回了Task<T>。当然,现在[Task.Run]将返回一个Task<Task<T>>,因此我们使用.NET 4中的Unwrap方法将Task<Task<T>>转换为Task<T>...
斯蒂芬·图布
以下是如何使用它:
AsyncLazy<MyClass> myObject = new AsyncLazy<MyClass>(() => new MyClass());
// later...
MyClass result = await myObject.Value;
在上面的代码片段中,myObject是一个AsyncLazy<MyClass>实例。在首次访问myObject.Value之前,该new MyClass()调用不会运行,并且它将异步运行。
结论
异步延迟初始化是一种强大的技术,可以帮助您使用C#编写更高效、响应更灵敏的应用程序。通过组合Lazy<T>和Task<T>类,您可以将昂贵对象的创建推迟到需要它们时,并且这样做不会阻塞主线程。
但是,像任何工具一样,它应该明智地使用。过度使用延迟初始化(无论是同步还是异步)可能会导致其自身的问题,例如内存使用量增加,如果处理不当,可能会出现死锁。此外,随着复杂性的增加,围绕容错和如何管理错误的考虑因素也成为一项挑战。
本文最初发布于 Async Lazy In C# - Great Power With Great Responsibility
https://www.codeproject.com/Articles/5366567/Async-Lazy-In-Csharp-With-Great-Power-Comes-Great