1

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?

9
  • Do you have to preload all the images at once, or is it a continuous process? Commented Mar 16, 2020 at 10:10
  • 1
    You can interrogate the Task returned from Task.Run() to see if it has finished, using Task.Wait(0). Commented Mar 16, 2020 at 10:10
  • @TheodorZoulias I can spread it out if I like. Commented Mar 16, 2020 at 10:12
  • @MatthewWatson Tempting suggestion, I hadn't thought of that :) Commented Mar 16, 2020 at 10:12
  • 1
    How about using a ConcurrentDictionary instead of a normal Dictionary? You could have a background process adding rendered images to the dictionary, and on the UI thread just grab the images using the TryGetValue method. This class is thread-safe, so no synchronization is needed. Commented Mar 16, 2020 at 11:05

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

9 Comments

Really nice - I was headed in a similar direction, although without the SemaphorSlim. Question though - is there much overhead in Task.Running each Preload() individually? It there a reason why you did it that way and not all of them in bulk inside a Task.Run? It's not important to me to split up the work as long as it runs in the background.
@AlexanderHøst the overhead of Task.Run is very small, and should be totally dwarfed by the work of creating the bitmap. To get an idea how minuscule it is, if hypothetically you had no work to do, then the pure overhead would limit your throughput to around 1,000,000 tasks per second (in a mediocre PC).
According to Stephen Cleary Task.Run in ASP.NET is problematic becauses it messes up the heuristics of the ASP.NET engine, since it uses the same ThreadPool for serving web requests. It is also bad for spawning fire-and-forget tasks because the engine doesn't know about them and may kill them at any moment by recycling the application. But for desktop apps Task.Run is OK. If you fire too many of them you could cause ThreadPool starvation though. This is why I included the SemaphoreSlim in the solution.
No, It's just an example, nothing special. Generally you don't want to have more CPU-bound tasks running concurrently, than the available processors/cores of the local machine (Environment.ProcessorCount). If preloading bitmaps is a low priority job, and you are doing other important heavy computations at the same time, or the external library does, you could configure the MySvgLibrary with degreeOfParallelism: 1 to reduce to a single core the max CPU resources it consumes.
Btw a ThreadPool starvation is just a temporary problem, because the ThreadPool when starved injects new threads at a rate of one every 500 msec, so eventually there will be enough threads to serve all tasks. But this scenario could cause awkward temporary hiccups. For example a System.Timers.Timer could stop firing regularly for a while, because it's Elapsed event is running in ThreadPool threads. If you are anticipating such a scenario you can preemptively increase the pool of threads by calling the ThreadPool.SetMinThreads method during the app initialization.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.