Блокировка вокруг лямбда-обработчика событий - PullRequest
0 голосов
/ 25 марта 2011

Мне нужно синхронизировать последовательность операций, которая содержит асинхронную часть. Метод просматривает кэш изображений и возвращает изображение, если оно там есть (в действительности вызывает обратный вызов). В противном случае он должен загрузить его с сервера. Операция загрузки является асинхронной и запускает событие по завершении.

Это (упрощенный) код.

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);
    }
}

Ответы [ 3 ]

1 голос
/ 25 марта 2011

У вас есть еще одна потенциальная проблема здесь. Этот код:

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 .

1 голос
/ 25 марта 2011

Лямбда-выражение преобразуется в делегат внутри блокировки, но тело лямбда-выражения не автоматически получит блокировку для монитора Cache, когда делегат казнены. Таким образом, вы можете хотите:

server.ImageDownloaded += (s, e) =>
{
    lock (Cache)
    {
        Cache.Add(e.Bitmap, e.Name);
    }
    onGetImage(e.Bitmap);
}
0 голосов
/ 25 марта 2011

Почему бы вам просто не сохранить коллекцию имен файлов изображений, которые загружаются, и иметь код для потока:

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 
        } 
    } 
}

Это более или менее стандартный шаблон монитора илибыть, если вы реорганизовали лямбда-выражение в функцию-член, такую ​​как GetImage.Вы должны действительно сделать это.Это облегчит рассуждение о логике монитора.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...