任务对象在完成后很长一段时间都在身边,这有什么缺点吗

本文关键字:在身边 什么 缺点 一段时间 对象 任务 | 更新日期: 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%确定它们已经完成,并且没有其他人在使用它们。

我的担忧如下:

  1. 这是一个好的使用模式吗
  2. 它有缺点或陷阱吗?(例如内存泄漏、潜在死锁、池资源锁定等)
  3. 相反,我应该打开任务的结果并直接存储它们吗(因此,在我看来,增加了代码的复杂性)

任务对象在完成后很长一段时间都在身边,这有什么缺点吗

  1. 如果它为您带来了更干净、更清晰的代码,那么是的,这是一个很好的模式
  2. 唯一真正的潜在缺点是这些任务不会被垃圾收集器收集。只要你使用这种模式几次,而不是数千或数百万次,对你来说可能永远不会有问题*(不必担心死锁或线程池资源,因为完成的任务就是完成的。它永远不会有另一个线程处理它,因为任务是不可重用的)
  3. 只要记忆不成问题,就没有理由这么做

*另一个缺点是,如果多次await一个出错的任务,每次都会抛出异常。这可能有问题,但这取决于您的特定异常处理