锁定lambda事件处理程序

时间:2011-03-25 14:51:28

标签: c# multithreading lambda

我需要同步包含异步部分的一系列操作。 该方法查看图像缓存并返回图像(如果它在那里)(实际调用回调)。否则它必须从服务器下载它。下载操作是异步的,并在完成时触发事件。

这是(简化)代码。

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

3 个答案:

答案 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之类的成员函数。你应该真的这样做。它将使监视器逻辑更容易推理。