Here's the deal:
I have files on remote server that ought to be accessed by different threads within my application. The former developer used to upload them thousand of times and I decided to change it slightly.
This is a fully operational example that mimics the file loaded I have so far:
public class FileLoader<T>
{
private object locker = new object();
/// <summary>
/// Key - Path to the file / Value - Task that is loading the file by the path
/// </summary>
private ConcurrentDictionary<string, Task<T>> fileDownloads = new ConcurrentDictionary<string, Task<T>>();
// counters for the test
private int countOfDownloadsStarted = 0;
public int CountOfDownloadsStarted => countOfDownloadsStarted;
private int countOfAwaitings = 0;
public int CountOfAwaitings => countOfAwaitings;
public async Task<T> GetTileFromFile(string path)
{
var exist = fileDownloads.TryGetValue(path, out Task<T> download);
if (!exist)
{
Monitor.Enter(locker);
exist = fileDownloads.TryGetValue(path, out download);
if (!exist)
{
Interlocked.Increment(ref countOfDownloadsStarted);
download = Task.Run(() =>
{
// mimic file loading by a given path
Thread.Sleep(1000);
return default(T);
});
fileDownloads.TryAdd(path, download);
}
Monitor.Exit(locker);
}
Interlocked.Increment(ref countOfAwaitings);
return await download;
}
}
And a program class with entry point for you to test it by your own:
class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"Test: {i + 1}");
Foo();
}
Console.ReadLine();
}
static async Task Foo()
{
var paths = new List<string>();
// creating fake paths
var pathsToReplicate = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (int i = 0; i < pathsToReplicate.Length; i++)
{
for (int j = 0; j < pathsToReplicate[i]; j++)
{
paths.Add(pathsToReplicate[i].ToString());
}
}
var fileLoader = new FileLoader<bool>();
try
{
Parallel.ForEach(paths, async (p) => await fileLoader.GetTileFromFile(p));
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.GetBaseException().Message}");
}
Console.WriteLine($"Count of downloads matches: {fileLoader.CountOfDownloadsStarted == pathsToReplicate.Length}");
Console.WriteLine($"Count of awaitings matches: {fileLoader.CountOfAwaitings == pathsToReplicate.Sum(p => p)}");
}
}
If you launch it as it is - you'll pass the test ten times and the numbers will match. But the problem here is that I use the same lock object even when the paths that are processing are different. It obviously causes redundant context switching, hence the resources wasting.
Firstly I thought that it would be okay to use the paths themselves as they're strings and they're immutable. But then I realized that it's not a good idea because of the same reason. Someone outside can easily lock the same object.
So here I am, looking for an elegant solution that, I am sure, lies on the surface.