【问题标题】:How to Read/Generate+Read file in thread-safe way in C#如何在 C# 中以线程安全的方式读取/生成+读取文件
【发布时间】:2015-02-12 14:06:51
【问题描述】:

我正在使用 .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;

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

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

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

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

    }
  }
}

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

【问题讨论】:

  • 你有不同的线程同时访问同一个文件?
  • 请定义并发请求?您可以使用基于文件的锁定机制(先独占打开写入文件,第一个打开它的人 - 完成工作,其他人 - 跳过),这意味着您不需要内存锁定。
  • @YuvalItzchakov 是的,问题是他们都开始创建相同大小的缩略图(保存相同的文件)
  • 互斥锁是解决方案
  • 您可以创建一个临时文件并在完成后将其重命名为目标名称。如果重命名失败,则丢弃临时文件,因为目标文件已经存在。应该在 Windows + NTFS 上工作,但我不能 100% 确定这种方法在某些文件系统/操作系统上是否安全。

标签: c# .net multithreading thread-safety


【解决方案1】:

您似乎需要一个线程安全的缩略图管理器。 IE。一个本身了解如何协调对文件的访问的类。

一个简单的版本可能如下所示:

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,这样该方法可以在需要时异步完成而不会阻塞调用线程。您的问题中没有足够的上下文来确切地知道那会是什么样子,但我想您可以弄清楚那部分。

【讨论】:

  • 感谢您的回答!
  • 但是我不想在内存中保留任何有关缩略图的信息,因为那是临时文件,我会有很多。
  • @U10:我不认为它们是临时文件——你没有说过以后要删除它们。您可能会删除 _thumbnails 字典并检查 expected 文件名(在文件系统上),在检查当前是否有任何东西正在做缩略图(在这种情况下,文件可能存在但正在处理中)被写)。这将具有处理 Web 应用重新启动的情况的额外好处。
  • @U10:您的问题中没有提到临时性质。在什么情况下会删除文件?发生这种情况时,您可以从 _thumbnails 字典中删除该条目吗?请注意,代码已经处理了重新启动的情况,因为如果文件已经存在,resize 只会返回名称。但乔恩是正确的......你可以完全跳过 _thumbnails 字典(这是一个小的优化),并简单地检查文件本身的存在作为检查。
  • @PeterDuniho 对不起,我没有提到文件将被删除的条件,基本上不会发生,但无论如何我不想将 _thumbnails 集合存储在内存中,所以我要签入文件系统。
猜你喜欢
  • 2020-03-03
  • 2013-06-30
  • 1970-01-01
  • 2019-04-13
  • 2014-02-09
  • 1970-01-01
  • 2012-02-07
相关资源
最近更新 更多