等待一个已经 运行 的任务

Await an already running task

我的应用有一个像全局资源这样的概念。我的解决方案中的每个部分都需要包含对象集合。其中一些在启动时加载,其他在第一次访问时加载。

我的问题是第二种情况(延迟加载的集合)。下面是一个测试项目:

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

我遇到的问题是 class GlobalResources 方法 LoadAsync。在那里您可以找到代表我当前编码方式的注释“A”。这种方式是一个问题,因为如果发生第二个加载请求,那么我有两个数据库调用。

您可以在“B”部分找到我的解决方案。我想将第一个请求的第一个任务存储在一个字段中。如果有第二次调用,它应该等待存储的任务,以防止它第二次调用数据库。

我的问题是我不知道如何编码。如果我手动使用任务,我认为异步等待模式不起作用。或者你有什么想法吗?

首先,await 同一个 任务多次执行是可以的,它不会损害您的数据库。因此,至少,您可以缓存这样的任务(无论如何您都在锁定,所以它是安全的)并在以后重用它。顺便说一下,这可以让您最大限度地减少锁定时间。

其次,由于您的资源根据定义是全局的,因此预先计算长运行ning 查询似乎是合理的。这意味着您 运行 在构造函数中执行适当的任务(无需 await 执行)并且不需要锁定,根本不需要。

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

根据评论,我可以更改 B 块,使其可编译。您可以在下面找到代码:

正如 damien-the-unbeliever 在评论中所说,发布的解决方案不是线程安全的!少了一些锁。

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