启动后台任务并立即返回

本文关键字:返回 后台任务 启动 | 更新日期: 2023-09-27 18:12:11

我有一个名为DownloadFileAsync(url, Func<RemoteFileResponse, Task> onDownloadFinished)的方法,它执行以下操作:

  • 检查缓存,如果找到,立即返回缓存,并启动后台任务查看缓存是否需要更新
  • 如果没有找到,返回null并启动后台任务异步下载文件
  • 文件下载后,它回调onDownloadFinished处理程序。

这对我来说感觉很脏,因为我需要在后台线程上执行下载,但我不能等待它,因为我希望能够立即返回缓存的文件。问题是,如果我这样做,我将失去任何异常上下文。

我能想到的一些选项:

  • 使用IProgress接口返回缓存文件,然后在下载完成后返回下载结果。
  • 将方法分为两个调用(一个获取缓存文件)另一个下载/更新(不是首选的方式,因为我想保持我的接口到一个方法)。

我想知道是否有人有任何其他的建议,我怎么才能做到这一点?

我的方法的伪代码:
Task<IFile> async DownloadFileAsync(url, Func<RemoteFileResponse, Task> onDownloadFinished)
{
   var cache = await CheckCacheAsync(url);
   // don't await this so the callee can use the cached file right away 
   // instead return the download result on the download finished callback
   DownloadUrlAndUpdateCache(url, onDownloadFinished);
   return cache
}

启动后台任务并立即返回

如果您的缓存是内存缓存,那么更容易缓存任务而不是它们的结果:

ConcurrentDictionary<Url, Task<IFile>> cache = ...;
Task<IFile> DownloadFileAsync(url) // no async keyword
{
  return cache.GetOrAdd(url, url => DownloadUrlAsync(url));
}
private async Task<IFile> DownloadUrlAsync(url)
{
  ... // actual download
}

逻辑上,GetOrAdd正在这样做(但以线程安全和更有效的方式):

if (cache.ContainsKey(url))
  return cache[url];
cache[url] = DownloadUrlAsync(url);
return cache[url];

但是,请注意,这将缓存完整的任务,因此下载异常也会被缓存。

我建议创建一个新的类FileRetriever,它将包含缓存的文件和一个任务,该任务将在从服务器检索到文件的最新版本时完成。

像这样:

public class FileRetriever
{
    public IFile CachedFile { get; private set; } 
    // Indicate if the CachedFile is the latest version of the file. If not, 
    // then LatestFileTask will complete eventually with the latest revision 
    public bool IsLatestFileVersion { get; private set; } 
    public Task<IFile> LatestFileTask { get; private set; } 
    public FileRetriever(IFile file)
    {
        IsLatestFileVersion = true;
        CachedFile = file;
        LatestFileTask = Task.FromResult(file);
    }
    public FileRetriever(IFile file, Task<IFile> latestFileTask)
    {
        IsLatestFileVersion = false;
        CachedFile = file;
        LatestFileTask = latestFileTask;
    }
}

要初始化FileRetriever对象,首先要检查缓存,看看文件是否已经存在。

Task<FileRetriever> async GetFileRetrieverAsync(url)
{
    IFile file = GetFileFromCache();
    if (file == null)
    {
         // File is not present in cache. Download it asynchronously and await it 
         // before returning the FileRetriever
         IFile file = await DownloadUrlAndUpdateCacheAsync(url);
         return new FileRetriever(file);
    }
    // File is present in cache but a new revision might be present on server. 
    // Start the download task but don't await for its completion. 
    Task<IFile> task = DownloadUrlAndUpdateCacheAsync(url);
    return new FileRetriever(file, task);
}

然后,你可以这样使用:

var fileRetriever = await GetFileRetrieverAsync(url);
DoSomethingWithFile(fileRetriever.CachedFile);
if (!fileRetriever.IsLatestFileVersion)
{
    // A new revision of the file might be present on the server.
    // Wait for the download before updating the UI with the new file revision
    fileRetriever.LatestFileTask.ContinueWith(t => DoSomethingWithFile(t.Result)); 
}

请注意,我没有测试过这个,如果文件下载失败,应该添加错误处理。