C# 启动后台任务并立即返回

C# 启动后台任务并立即返回,c#,asynchronous,async-await,C#,Asynchronous,Async Await,我有一个名为DownloadFileAsync(url,Func-ondownloadfished)的方法,它执行以下操作: 检查缓存,如果找到,立即返回缓存并启动后台任务以查看是否需要更新缓存 如果未找到,则返回null并启动后台任务以异步下载文件 下载文件后,它会调用onDownloadFinished处理程序 这让我感觉很脏,因为我需要在后台线程上执行下载,但我不能等待它,因为我希望能够立即返回缓存文件。问题是,如果这样做,我会丢失任何异常上下文 我能想到的一些选择: 使用IProg

我有一个名为
DownloadFileAsync(url,Func-ondownloadfished)
的方法,它执行以下操作:

  • 检查缓存,如果找到,立即返回缓存并启动后台任务以查看是否需要更新缓存
  • 如果未找到,则返回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
}
Task async DownloadFileAsync(url,Func-onDownloadFinished)
{
var cache=await-CheckCacheAsync(url);
//不要等待,这样被调用方就可以立即使用缓存文件
//而是在下载完成回调上返回下载结果
DownloadUrl和UpdateCache(url,onDownloadFinished);
返回缓存
}

如果您的缓存是内存缓存,那么缓存任务比缓存结果更容易:

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
}

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

我建议创建一个新的类FileRetriever,该类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;
    }
}

请注意,我没有对此进行测试,如果文件下载失败,则应添加错误处理。

请包括
DownloadFileAsync(url,Func onDownloadFinished)的返回类型。
只返回
任务
,而不是使用回调委托。@ScottChamberlain我用伪代码更新了问题,说明了我正在尝试做什么。@JustinHorst该方法只能返回一个
任务
。如果该项被缓存,那么该任务将很快完成并具有缓存值。如果它没有被缓存,那么完成任务只需要更长的时间。调用者不需要知道或关心值是否来自缓存,就像编写同步方法时设置的那样。@JustinHorst然后总是返回两个任务,一个用于缓存版本,一个用于未缓存版本,而不是一个任务和一个回调。Stephen感谢您的响应!然而,我有以下要求。即使结果在缓存中,我仍然希望发出webrequest以查看我拥有的版本是否仍然是新的(即,我在下一个请求中发送Etag,如果图像不再是新的,则重新下载图像)。另外,我的缓存在另一个服务中(不在内存中)。@Stephen,我认为最好将
DownloadUrlAsync(url)
包装成
new Lazy(()=>DownloadUrlAsync(url))
,否则在使用同一键的并发调用期间,ConcurrentDictionary.GetOrAdd的值工厂将被计算多次
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;
    }
}
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)); 
}