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

Dmi*_*ets 8 .net c# multithreading thread-safety

我正在使用.NET Framework v4.5.

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


使用案例:

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

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

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


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

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

目前的实施如下:

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

    _physicalFileManager.WriteToFile(cachedImageFilepath, resizedImage);
}

return cachedImageFilepath;
Run Code Online (Sandbox Code Playgroud)

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

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

但无论如何,我发现在释放锁之后文件是否存在时双重检查有问题:

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

    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

Pet*_*iho 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;
}
Run Code Online (Sandbox Code Playgroud)

换句话说,管理器首先检查它是否知道必要的文件。如果是,则意味着该文件已创建,并且仅返回路径。

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

无论哪种情况,Task都会等待表示操作的信息。该方法在此时返回;当操作完成时,该方法恢复执行,将结果文件的名称添加到已完成文件的字典中,并从正在进行的字典中删除已完成的任务。

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