Search code examples
c#asynchronoustasktask-queue

How can I queue the Task result of an async method without running it?


If I have a class that holds a Queue of Tasks to be executed later, AND I have an async Task<T> method, how can I enqueue that async method without executing it?

I want to "delay" this task, and be sure the caller sees it run later just as if it was awaited there in the method body. --- The caller should not know that I have enqueue the task for later.

Right now, if my queue is full, I construct and return new a Task<T> here that is not running, which is returning the .Result of my private async method:

public async Task<T> ExecuteAsync<T>(T transaction) {
    if (mustDelay) {
        Task<T> task = new Task<T>(t => executeAsync((T) t).Result, transaction);
        enqueue(task);
        return await task;
    }
    return await executeAsync(transaction);
}

private async Task<T> executeAsync<T>(T transaction) {
    await someWork();
    return transaction;
}

When some other task completes, I Dequeue and Start() that enqueued task:

dequeuedTask.Start();

Does this ensure the caller sees the same synchronization as if just returning the awaited result from the method?


Solution

  • How can I queue the Task result of an async method without running it?

    Short answer: you can't. Calling an async method executes that method. It necessarily will start running. If you want to be able to defer the call, you need to wrap it in something that will do that.

    One example might be Func<Task<T>>, except that the little tiny bit of code you deigned to share with us suggests you want to be able to return a promise (Task<T>) as well that represents this call you'll make in the future. You can wrap the whole thing in another task, like in your example code, but IMHO that's a pretty heavy-handed approach, since it ties up (and possibly creates new) a thread pool thread just for the purpose of calling the async method.

    A better way (IMHO) to accomplish that is to use TaskCompletionSource<T>. You can store a Func<Task> in a queue, which uses the return value to set the TaskCompletionSource<T>, then when you decide you can start the task, invoke the Func<Task>.

    Something like:

    public Task<T> ExecuteAsync<T>(T transaction) {
        if (mustDelay) {
            TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
    
            enqueue(async () =>
            {
                tcs.SetValue(await executeAsync(transaction));
            });
            return tcs.Task;
        }
        return executeAsync(transaction);
    }
    

    Note that here, there's no need for ExecuteAsync<T>() to be async. You either return the TaskCompletionSource<T>'s task, or the task returned by the executeAsync<T>() method (by the way, having two methods with names that differ only in letter-casing is IMHO a horrible idea).

    Note also that your queue will store Func<Task> objects, or maybe even Action objects (it's generally frowned upon async void methods such as the anonymous method above, but you didn't show any exception handling in the first place, so maybe you'll find it works fine in this case). When you dequeue an item, you'll invoke that delegate. Depending on your needs, this will either be "fire-and-forget" (if you store Action delegates) or the method that dequeues and invokes the delegate may await the return value of the delegate (if you are storing Func<Task> delegates).

    Unfortunately, your question is fairly vague. So it's not possible to offer much more than this. If you need additional help, please improve the question so it includes a good Minimal, Complete, and Verifiable code example that shows clearly what you're trying to accomplish and what specifically you're having trouble figuring out, along with an appropriate explanation to describe that in detail.