Search code examples
c#asynchronousbackground-process

Loading resource on background thread in non-async context


I'm working with an external library that expects me to produce bitmaps when it calls GetImage for the following interface it exposes:

public interface IImageProvider
{
    Bitmap GetImage(string imageId);
}

The library asks for them in bulk - i.e. it calls GetImage() repeatedly on the UI thread, thus creating substantial UI lag. Now, I have time to pre-render images for each of these ids before the library actually asks for them. I would like to do so on a background thread, but I am obviously not in a position to return a Task<Bitmap> back through the interface.

What I'm essentially trying to achieve is summmarized below: Let's say I create a library - MySvgLibrary:

public interface MySvgLibrary
{
    void Preload();
    Dictionary<string, Bitmap> Library { get; }
}

I now want to Task.Run(() => _myLibrary.Preload() }. Given that I don't think I can use async/await here (since I cannot return a Task<Bitmap>, I don't see how I can use, say, a TaskCompletionSource in this context. How do I know that Preload is finished? I mean, I could check if Library is null and spin until it isn't (and that does work, btw) but that approach makes me nauseous. Suggestions?


Solution

  • Here is an implementation of the MySvgLibrary class. It uses a ConcurrentDictionary for storing the bitmaps, and a SemaphoreSlim for controlling the degree of parallelism (how many threads are allowed to create images in parallel).

    public class MySvgLibrary
    {
        private readonly ConcurrentDictionary<string, Task<Bitmap>> _dictionary;
        private readonly SemaphoreSlim _semaphore;
    
        public MySvgLibrary(int degreeOfParallelism = 1)
        {
            _dictionary = new ConcurrentDictionary<string, Task<Bitmap>>();
            _semaphore = new SemaphoreSlim(degreeOfParallelism);
        }
    
        public Task<Bitmap> GetImageAsync(string key)
        {
            return _dictionary.GetOrAdd(key, _ => Task.Run(async () =>
            {
                await _semaphore.WaitAsync().ConfigureAwait(false);
                try
                {
                    return CreateImage(key);
                }
                finally
                {
                    _semaphore.Release();
                }
            }));
        }
    
        public Bitmap GetImage(string key)
        {
            return GetImageAsync(key).GetAwaiter().GetResult();
        }
    
        public void PreloadImage(string key)
        {
            var fireAndForget = GetImageAsync(key);
        }
    
        private Bitmap CreateImage(string key)
        {
            Thread.Sleep(1000); // Simulate some heavy computation
            return new Bitmap(1, 1);
        }
    }
    

    Usage example:

    var svgLibrary = new MySvgLibrary(degreeOfParallelism: 2);
    svgLibrary.PreloadImage("SomeKey"); // the preloading happens in background threads
    Bitmap bitmap = svgLibrary.GetImage("SomeKey"); // blocks if the bitmap is not ready yet
    

    You should put the actual code that produces the images into the CreateImage method. In case an exception is thrown by the CreateImage, the exception will be propagated and rethrown when the GetImage is called.