我需要同步包含异步部分的一系列操作。 该方法查看图像缓存并返回图像(如果它在那里)(实际调用回调)。否则它必须从服务器下载它。下载操作是异步的,并在完成时触发事件。
这是(简化)代码。
private Dictionary<string, Bitmap> Cache;
public void GetImage(string fileName, Action<Bitmap> onGetImage)
{
if (Cache.ContainsKey(fileName))
{
onGetImage(Cache[fileName]);
}
else
{
var server = new Server();
server.ImageDownloaded += server_ImageDownloaded;
server.DownloadImageAsync(fileName, onGetImage); // last arg is just passed to the handler
}
}
private void server_ImageDownloaded(object sender, ImageDownloadedEventArgs e)
{
Cache.Add(e.Bitmap, e.Name);
var onGetImage = (Action<Bitmap>)e.UserState;
onGetImage(e.Bitmap);
}
问题:如果两个线程几乎同时调用GetImage,它们将调用服务器并尝试将相同的图像添加到缓存中。我应该做的是在GetImage的开头创建锁,并在server_ImageDownloaded处理程序的末尾释放它。
显然这对于lock
构造是不可行的,并且它没有意义,因为在任何情况下都很难确保锁被释放。
现在我认为我可以做的是使用lambda而不是事件处理程序。通过这种方式,我可以锁定整个部分:
我必须在DownloadImage方法的开头锁定Cache字典,并仅在ImageDownloaded事件处理程序的末尾释放它。
private Dictionary<string, Bitmap> Cache;
public void GetImage(string fileName, Action<Bitmap> onGetImage)
{
lock(Cache)
{
if (Cache.ContainsKey(fileName))
{
onGetImage(Cache[fileName]);
}
else
{
var server = new Server();
server.ImageDownloaded += (s, e) =>
{
Cache.Add(e.Bitmap, e.Name);
onGetImage(e.Bitmap);
}
server.DownloadImageAsync(fileName, onGetImage); // last arg is just passed to the handler
}
}
}
这样安全吗?或者在执行GetImage后立即释放锁定,使lambda表达式解锁?
有没有更好的方法来解决这个问题?
解
最后,解决方案是所有答案和评论的混合,遗憾的是我无法将所有答案都标记为答案。所以这是我的最终代码(为了清楚起见,删除了一些空检查/错误情况/等)。
private readonly object ImageCacheLock = new object();
private Dictionary<Guid, BitmapImage> ImageCache { get; set; }
private Dictionary<Guid, List<Action<BitmapImage>>> PendingHandlers { get; set; }
public void GetImage(Guid imageId, Action<BitmapImage> onDownloadCompleted)
{
lock (ImageCacheLock)
{
if (ImageCache.ContainsKey(imageId))
{
// The image is already cached, we can just grab it and invoke our callback.
var cachedImage = ImageCache[imageId];
onDownloadCompleted(cachedImage);
}
else if (PendingHandlers.ContainsKey(imageId))
{
// Someone already started a download for this image: we just add our callback to the queue.
PendingHandlers[imageId].Add(onDownloadCompleted);
}
else
{
// The image is not cached and nobody is downloading it: we add our callback and start the download.
PendingHandlers.Add(imageId, new List<Action<BitmapImage>>() { onDownloadCompleted });
var server = new Server();
server.DownloadImageCompleted += DownloadCompleted;
server.DownloadImageAsync(imageId);
}
}
}
private void DownloadCompleted(object sender, ImageDownloadCompletedEventArgs e)
{
List<Action<BitmapImage>> handlersToExecute = null;
BitmapImage downloadedImage = null;
lock (ImageCacheLock)
{
if (e.Error != null)
{
// ...
}
else
{
// ...
ImageCache.Add(e.imageId, e.bitmap);
downloadedImage = e.bitmap;
}
// Gets a reference to the callbacks that are waiting for this image and removes them from the waiting queue.
handlersToExecute = PendingHandlers[imageId];
PendingHandlers.Remove(imageId);
}
// If the download was successful, executes all the callbacks that were waiting for this image.
if (downloadedImage != null)
{
foreach (var handler in handlersToExecute)
handler(downloadedImage);
}
}
答案 0 :(得分:1)
lambda表达式转换为锁定中的委托,但lambda表达式的 body 将 not 自动获取Cache
监视器的锁定当委托被执行时。所以你可能想要:
server.ImageDownloaded += (s, e) =>
{
lock (Cache)
{
Cache.Add(e.Bitmap, e.Name);
}
onGetImage(e.Bitmap);
}
答案 1 :(得分:1)
你这里有另一个潜在的问题。这段代码:
if (Cache.ContainsKey(fileName))
{
onGetImage(Cache[fileName]);
}
如果某个其他线程在您调用ContainsKey
之后但在执行下一行之前从缓存中删除了图像,则会崩溃。
如果您在并发线程可以读写的多线程上下文中使用Dictionary
,那么您需要使用某种锁保护每次访问。 lock
很方便,但ReaderWriterLockSlim会提供更好的效果。
我还建议您重新编写以上代码:
Bitmap bmp;
if (Cache.TryGetValue(fileName, out bmp))
{
onGetImage(fileName);
}
如果您运行的是.NET 4.0,那么我强烈建议您考虑使用ConcurrentDictionary。
答案 2 :(得分:0)
为什么不保存正在下载的图像文件名集合,并为线程编写代码:
public void GetImage(string fileName, Action<Bitmap> onGetImage)
{
lock(Cache)
{
if (Cache.ContainsKey(fileName))
{
onGetImage(Cache[fileName]);
}
else if (downloadingCollection.contains(fileName))
{
while (!Cache.ContainsKey(fileName))
{
System.Threading.Monitor.Wait(Cache)
}
onGetImage(Cache[fileName]);
}
else
{
var server = new Server();
downloadCollection.Add(filename);
server.ImageDownloaded += (s, e) =>
{
lock (Cache)
{
downloadCollection.Remove(filename);
Cache.Add(e.Bitmap, e.Name);
System.Threading.Monitor.PulseAll(Cache);
}
onGetImage(e.Bitmap);
}
server.DownloadImageAsync(fileName, onGetImage); // last arg is just passed to the handler
}
}
}
这或多或少是标准的监视器模式,或者如果你将lambda表达式重构为GetImage
之类的成员函数。你应该真的这样做。它将使监视器逻辑更容易推理。