任务对象在完成后很长一段时间都在身边,这有什么缺点吗
本文关键字:在身边 什么 缺点 一段时间 对象 任务 | 更新日期: 2023-09-27 18:22:26
我发现自己养成了将Task
对象保存为结果容器的习惯。
到目前为止,我还没有发现任何缺点,我发现代码比在任务完成后使用单独的变量来存储结果更清晰。
下面是几个用法示例。虽然我不认为它真的有相关性,但它们已经作为MVVM应用程序中视图模型的一部分。(请注意,这不是实际的工作代码,我只是试图勾勒出模式。)
-
早期初始化
SlowClient
是一些需要几秒钟才能连接到WCF或REST的类服务因此,我通过任务尽快初始化它。当需要客户端时,等待任务,生成初始化的SlowClient
(如果任务完成,则立即执行,或者在等待任务完成后执行)。所以我会做这样的事情:
public class SomeViewModel { private Task<SlowClient> _slowClientInitializerTask; public SomeViewModel() { _slowClientInitializerTask = Task.Run(() => CreateAndInitSlowClient()); } private SlowClient CreateAndInitSlowClient() { // SlowClient instantiation and initialization, taking a few seconds... } // Task result consumer example void OnSomeCommandExecuted(object parameter) { try { var client = await _slowClientInitializerTask; // do something with the client } catch { // may re-create the task if the client ceases to be valid } } }
如
OnSomeCommandExecuted
所示,使用SlowClient
的每种方法都将简单地执行:var client = await _slowClientInitializerTask;
如果由于某种原因,结果不再有效(连接到被丢弃的服务),我只会运行一个新的任务并将其分配给
_slowClientInitializerTask
——就像构造函数中显示的代码。该模式的替代方案是创建一些额外的
_slowClient
变量,该变量在任务完成后进行更新,因此每次使用时都需要进行检查。例如:if (_slowClient == null) _slowClient = await _slowClientInitializerTask;
我看不到任何好处,只是增加了复杂性。
-
后台工作人员
一个更复杂的示例使用任务来处理图像,创建一个包含重新调整大小的图像的新文件。必须生成包含这些图像的报告;它通过图像文件的路径访问图像文件,并且必须尽可能使用重新调整大小的版本——如果不是,则使用原始图像。因此,我需要能够将原始图像的路径映射到它们的重新调整大小的版本。
// Key: original image path; Value: task returning the re-sized image path private Dictionary<string, Task<string>> _resizeTasks; // Costly operation => Want to execute it asynchronously private string ResizeImage(string originalImagePath) { // returns the path of a temporary resized image file } // Command execution handler for instance => Launches image resize on background void OnAddImageExecuted(object parameter) { string path = parameter as string; if (!_resizeTasks.Keys.Contains(path)) _resizeTasks[path] = Task.Run(() => ResizeImage(path)); } // Generates a report consuming the images => Requires the result of the tasks void OnGenerateReportExecuted(object parameter) { try { foreach (var faulted in from entry in _resizeTasks where entry.Value.IsFaulted select entry.Key) _resizeTasks[path] = Task.Run(() => ResizeImage(path)); // Retry await Task.WhenAll(_resizeTasks.Values); // Wait for completion } catch { } // Ignore exceptions thrown by tasks (such as I/O exceptions) var imagePaths = _resizeTasks[path].Select(entry => entry.Value.Status == TaskStatus.RanToCompletion ? entry.Value.Result : entry.Key); // generate the report requiring the image paths }
实际实现使用
ConcurrentDictionary
,因为对图像的添加是异步执行的。此外,图像可以被移除并再次添加,因此存在用于当前添加的图像的单独列表,并且_resizeTasks
还用作先前重新调整大小的图像的高速缓存。
任务处理不是这里的主题,因为我可以稍后处理它们,无论如何,在这些情况下似乎没有必要,正如我需要处理任务吗来自.NET并行编程MSDN博客的文章:
没有。除非性能或可扩展性测试表明您需要根据使用模式来处理任务,以达到性能目标,否则不要麻烦处理任务。如果你确实发现需要处理它们,只有在容易的时候才这样做,即当你的代码中已经有了一个点,你可以100%确定它们已经完成,并且没有其他人在使用它们。
我的担忧如下:
- 这是一个好的使用模式吗
- 它有缺点或陷阱吗?(例如内存泄漏、潜在死锁、池资源锁定等)
- 相反,我应该打开任务的结果并直接存储它们吗(因此,在我看来,增加了代码的复杂性)
- 如果它为您带来了更干净、更清晰的代码,那么是的,这是一个很好的模式
- 唯一真正的潜在缺点是这些任务不会被垃圾收集器收集。只要你使用这种模式几次,而不是数千或数百万次,对你来说可能永远不会有问题*(不必担心死锁或线程池资源,因为完成的任务就是完成的。它永远不会有另一个线程处理它,因为任务是不可重用的)
- 只要记忆不成问题,就没有理由这么做
*另一个缺点是,如果多次await
一个出错的任务,每次都会抛出异常。这可能有问题,但这取决于您的特定异常处理