如何在C#中以线程安全的方式读取/生成+读取文件

时间:2015-02-12 14:06:51

标签: c# .net multithreading thread-safety

我正在使用 .NET Framework v4.5

我正在使用MagickImage库创建一种图像缩放器。


用例:

用户上传大图像(4k * 4k像素)并在不同大小(200 * 200像素,1200 * 1200像素)的不同位置使用。

所以我通过调整大图并将它们存储在磁盘上来按需生成这样的图像。

并发案例:用户上传图片,然后几个用户请求此图片的缩略图。在那一刻,每个用户请求开始创建已调整大小的缩略图,因为它尚不存在。完成调整大小的第一个线程将其保存到磁盘。由于该文件已被使用,所有其他线程都将获得异常。


在此之前它正在使用单线程,并且不需要线程安全。

但现在它将用于基于网络的项目,并发请求也是可能的

目前的实施如下:

if (!FileExists(cachedImageFilepath))
{
    byte[] resizedImage = _imageResizer.ResizeImage(originalImageFilepath, width, height);

    _physicalFileManager.WriteToFile(cachedImageFilepath, resizedImage);
}

return cachedImageFilepath;

简单方法就是使用锁定此操作,但在这种情况下,Resizer将在一段时间内仅调整一个图像的大小。

我看到的另一个变体是创建类似锁定机制的东西,它将通过字符串键锁定。

但无论如何,我发现在释放锁之后,如果文件存在,请仔细检查是否存在问题:

if (!FileExists(cachedImageFilepath)){
  lock(lockObjects[lockKey]){
    if (!FileExists(cachedImageFilepath)){

    }
  }
}

有没有一种好方法甚至.NET机制可以在没有开销的情况下做这样的事情?

1 个答案:

答案 0 :(得分:2)

看起来你需要的是一个线程安全的缩略图管理器。即一个本身就能理解如何协调对文件的访问的类。

简单版本可能如下所示:

class ThumbnailManager
{
    private Dictionary<Tuple<string, int, int>, string> _thumbnails =
        new Dictionary<Tuple<string, int, int>, string>();
    private Dictionary<Tuple<string, int, int>, Task<string>> _workers =
        new Dictionary<Tuple<string, int, int>, Task<string>>();
    private readonly object _lock = new object();

    public async Task<string> RetrieveThumbnail(string originalFile, int width, int height)
    {
        Tuple<string, int, int> key = Tuple.Create(originalFile, width, height);
        Task task;

        lock (_lock)
        {
            string fileName;

            if (_thumbnails.TryGetValue(key, out fileName))
            {
                return fileName;
            }

            if (!_workers.TryGetValue(key, out task))
            {
                task = Task.Run(() => ResizeFile(originalFile, width, height));
                _workers[key] = task;
            }
        }

        string result = await task;

        lock (_lock)
        {
            _thumbnails[key] = result;
            _workers.Remove(key);
        }

        return result;
    }
}

string ResizeFile(string originalImageFilepath, int width, int height)
{
    string cachedImageFilepath = GenerateCachedImageFilepath(originalImageFilepath);

    if (!FileExists(cachedImageFilepath))
    {
        byte[] resizedImage = _imageResizer.ResizeImage(originalImageFilepath, width, height);

        _physicalFileManager.WriteToFile(cachedImageFilepath, resizedImage);
    }

    return cachedImageFilepath;
}

换句话说,首先管理员检查它是否知道必要的文件。如果是,则表示文件已经创建,只返回路径。

如果没有,那么它检查的下一件事是查看是否正在创建必要的文件。毕竟,没有必要多次制作同一个文件!如果它尚未进行,则会启动Task来制作文件。如果它已在进行中,那么它只是检索表示该操作的Task

在任何一种情况下,都在等待代表该操作的Task。该方法在该点返回;当操作完成后,该方法继续执行,将生成的文件的名称添加到已完成文件的字典中,并从正在进行的字典中删除已完成的任务。

当然,它是一个async方法,调用者使用它的正确方法是在调用它时使用await,这样方法可以在需要时异步完成而不会阻塞调用线程。你的问题中没有足够的背景来确切地知道它会是什么样子,但我认为你可以把这一部分弄清楚。