与WhenAll并行执行Task时的任务缓存

本文关键字:任务 缓存 Task WhenAll 并行执行 | 更新日期: 2023-09-27 18:19:24

所以我有这个小代码块,将并行执行几个任务。

// no wrapping in Task, it is async
var activityList = await dataService.GetActivitiesAsync();
// Select a good enough tuple
var results = (from activity in activityList
               select new { 
                Activity = activity, 
                AthleteTask = dataService.GetAthleteAsync(activity.AthleteID)
               }).ToList(); // begin enumeration
// Wait for them to finish, ie relinquish control of the thread
await Task.WhenAll(results.Select(t => t.AthleteTask));
// Set the athletes
foreach(var pair in results)
{
  pair.Activity.Athlete = pair.AthleteTask.Result;
}

所以我正在下载每个给定活动的运动员数据。但也有可能我们多次请求同一个运动员。我们如何确保GetAthleteAsync方法将只在线获取实际数据,如果它还没有在我们的内存缓存?

目前我尝试在GetAthleteAsync方法内使用ConcurrentDictionary<int, Athelete>

private async Task<Athlete> GetAthleteAsync(int athleteID)
{
       if(cacheAthletes.Contains(athleteID))
             return cacheAthletes[atheleID];
       ** else fetch from web
}

与WhenAll并行执行Task时的任务缓存

您可以更改您的ConcurrentDictionary缓存Task<Athlete>而不仅仅是Athlete。记住,Task<T>是一个承诺——一个最终会导致T的操作。因此,可以缓存操作而不是结果

ConcurrentDictionary<int, Task<Athlete>> cacheAthletes;

那么,你的逻辑将是这样的:如果操作已经在缓存中,立即(同步)返回缓存的任务。如果不是,则开始下载,将下载操作添加到缓存中,并返回新的下载操作。注意,所有的"下载操作"逻辑都被移动到另一个方法:

private Task<Athlete> GetAthleteAsync(int athleteID)
{
  return cacheAthletes.GetOrAdd(athleteID, id => LoadAthleteAsync(id));
}
private async Task<Athlete> LoadAthleteAsync(int athleteID)
{
  // Load from web
}

这样,对同一个运动员的多个并行请求将得到相同的Task<Athlete>,并且每个运动员只下载一次。

您还需要跳过未成功完成的任务。这是我的片段:

ObjectCache _cache = MemoryCache.Default;
static object _lockObject = new object();
public Task<T> GetAsync<T>(string cacheKey, Func<Task<T>> func, TimeSpan? cacheExpiration = null) where T : class
{
    var task = (T)_cache[cacheKey];
    if (task != null) return task;          
    lock (_lockObject)
    {
        task = (T)_cache[cacheKey];
        if (task != null) return task;
        task = func();
        Set(cacheKey, task, cacheExpiration);
        task.ContinueWith(t => {
            if (t.Status != TaskStatus.RanToCompletion)
                _cache.Remove(cacheKey);
        });
    }
    return task;
}i

当缓存任务对象提供的值时,您希望确保缓存实现确保:

    没有并行或不必要的操作来获取一个值。在你的情况下,这是你的问题关于避免多个GetAthleteAsync相同的id。
  • 你不希望有负缓存(即缓存失败的结果),或者如果你想要它,它需要一个实现决策,你需要处理最终替换失败的结果。
  • 缓存用户不能从缓存中获得无效的结果,即使值在和await期间无效。

我有一篇关于缓存任务对象的博客文章,其中包含示例代码,确保了上述所有要点,并且可能对您的情况有用。基本上,我的解决方案是将Lazy<Task<T>>对象存储在MemoryCache中。