How to read / generate + read a file in thread safe mode in C #

I am using the .NET Framework v4.5 .

I am creating a resizer view using the MagickImage library.


Use case:

The user uploads a large image (4k * 4k pixels) and uses it in different places of different sizes (200 * 200 pixels, 1200 * 1200 pixels).

Therefore, I generate such images on demand by resizing large sizes and saving them to disk.

The case with concurrency: A user uploads an image, and then a couple of users request a thumbnail of this image. At this point, each user request begins to create a thumbnail because it does not yet exist. When the first thread that finished resizing saves it to disk. All other threads will receive an exception because the file is already in use.


Before that, he worked with a single thread , and there was no need for thread safety.

But now it can be used in a web project, and simultaneous requests are also possible .

The current implementation is as follows:

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

The easy way is to simply lock around this operation, but in this case, Resizer will only resize one image in one period of time.

Another option that I see is to create something like a locking mechanism that will be locked using a string key.

But in any case, I see a problem with double checking if the file exists after the lock is released, for example:

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

Is there a good way or even a .NET mechanism to do this without the overhead?

+8
multithreading c # thread-safety
source share
1 answer

It seems like you need a thumbnail streaming manager. That is, a class that itself understands how to coordinate file access.

A simple version might look like this:

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

In other words, the manager first checks to see if he knows about the file. If so, it means that the file has already been created and it simply returns the path.

If this is not the case, then the next thing he checks is to see if the necessary file is being created. In the end, there is nothing to do the same file more than once! If it is not already running, then it starts Task to make the file. If it is already running, it simply retrieves the Task representing this operation.

In either case, a Task representing the operation is expected. The method returns at this moment; when the operation is completed, the method resumes execution, adds the name of the received file to the dictionary of completed files and removes the completed task from the ongoing dictionary.

Naturally, this is an async method, the correct way for the caller to use it is for him to use await when calling, so that the method can be executed asynchronously, if necessary, without blocking the calling thread. There is not enough context in your question to know exactly how this will look, but I assume that you can understand this part.

+2
source share

All Articles