Alois
Alois

Reputation: 431

Await an already running task

My app has a concept like global resources. There are collections of objects contained which each part in my solution needs. Some of them are loaded at startup and other at first access.

My question goes to the second case (the lazy loaded collections). Below you see a test project:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Wellcome!");

        var globalResources = new GlobalResources();

        var toolClassA = new MyToolClass(globalResources);
        var toolClassB = new MyToolClass(globalResources);

        // This two "Init" calls can not be awaited in real project.
        // They could be invoked by two different buttons
        _ = toolClassA.InitAsync();
        _ = toolClassB.InitAsync();

        Console.WriteLine("Finish");
        Console.ReadLine();
    }
}

public class MyToolClass
{
    private readonly GlobalResources _globalResources;

    public MyToolClass(GlobalResources globalResources)
    {
        _globalResources = globalResources ?? throw new ArgumentNullException(
            nameof(globalResources));
    }

    public async Task InitAsync()
    {
        await _globalResources.LoadAsync();
        Console.WriteLine("awaited");
    }
}

public class GlobalResources
{
    public bool _isLoaded;
    public readonly object _loadLock = new object();

    private string _externalQueryResult;

    private Task _runningLoadTask;

    public async Task LoadAsync()
    {
        if (_isLoaded)
        {
            return;
        }

        lock (_loadLock)
        {
            if (_isLoaded)
            {
                return;
            }
        }

        // A: Current code:
        {
            _externalQueryResult = await LongRunningQuery();
        }

        // B: Wanting something like this:
        {
            // If no task is already running then start one.
            if (_runningLoadTask == null)
            {
                _runningLoadTask = LongRunningQuery();
                await _runningLoadTask.Start(); // How await it?
            }
            // If a task ist already running then no db call should happen.
            // Instead the currently running task should be awaited.
            else
            {
                await Task.WaitAll(_runningLoadTask); // ???
                return;
            }
        }

        lock (_loadLock)
        {
            _isLoaded = true;
        }
    }

    public async Task<string> LongRunningQuery()
    {
        // This method should only be called once.
        Console.WriteLine("Loading data from Database (
            this line should only be written once on console window).");

        await Task.Delay(5000);
        return "123";
    }
}

The problem I have is in class GlobalResources method LoadAsync. There you can find a comment "A" which represents the way I code currently. That way is a problem because if a second load request happens then I have two database calls.

My solution you can find in section "B". I want to store the first task of the first request in a field. If a second call comes in it should await the stored Task which prevent it from call the db a second time.

My problem is I don't know how to code it. I think the async await pattern does not work if I work with Tasks manually. Or have you any idea?

Upvotes: 0

Views: 888

Answers (2)

Alois
Alois

Reputation: 431

Based on the comments I was possible to change the B-block so it is compileable. You can find the code below:

As damien-the-unbeliever said in a comment, the posted solution is not thread safe! There are missing some locks.

// B: Wanting something like this:
{
    // If no task is already running then start one.
    if (_runningLoadTask == null)
    {
        _runningLoadTask = LongRunningQuery();
        await _runningLoadTask; 
    }
    // If a task ist already running then no db call should happen.
    // Instead the currently running task should be awaited.
    else
    {
        await _runningLoadTask; 
        // Alternative way:
        // await Task.WhenAll(_runningLoadTask); 
    }
}

Upvotes: 1

Zazaeil
Zazaeil

Reputation: 4104

First of all, it is OK to await the same task multiple times and it not gonna hurt your database. So, at least, you could cache such task (you're locking anyways, so it's safe) and later reuse it. That lets you to minimize locking time btw.

Secondly, since your resources are global by definition, it seems reasonable to precompute long-running query. Which means you run appropriate task right in the constructor (without awaiting it) and no locking is needed, not at all.

public MyConstructor()
{
    _myCachedTask = LongRunningTask(); // don't await it; hopefully, task is now running somewhere on the thread pool
}

...

public Task LongRunningTask() => _myCachedTask; // it's OK to await it multiple times, thread safe as well

Upvotes: 3

Related Questions