Мне нужно синхронизировать последовательность операций, которая содержит асинхронную часть.
Метод просматривает кэш изображений и возвращает изображение, если оно там есть (в действительности вызывает обратный вызов). В противном случае он должен загрузить его с сервера. Операция загрузки является асинхронной и запускает событие по завершении.
Это (упрощенный) код.
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
, и это не имело бы смысла, потому что было бы трудно гарантировать, что блокировка была освобождена в любом случае.
Теперь я решил использовать лямбду вместо обработчика событий. Таким образом, я могу поставить блокировку вокруг всего раздела:
Мне нужно заблокировать словарь Cache в начале метода DownloadImage и освободить его только в конце обработчика события 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, оставляя лямбда-выражение разблокированным?
Есть ли лучший подход для решения этой проблемы?
РЕШЕНИЕ
В итоге решение представляло собой смесь всех ответов и комментариев, к сожалению, я не могу отметить все из них как отмеченные. Итак, вот мой окончательный код (для ясности удалены некоторые нулевые проверки / случаи ошибок / и т. Д.).
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);
}
}