简体   繁体   中英

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?

Here's a simplified example:

async Task<T> GetResult<T>()
{
    DoRightAway();
    await DoLater();
    return DoEvenLater();
}

Task<T> task = GetResult<T>();
//task is running or complete

You'd like to defer GetResult<T>() 's execution by getting the incomplete task that it represents. The problem is that calling an async method starts running it, even when you don't await the call. The method runs to the first await , which is await DoLater() in this example.

To understand why you can't get the task represented by an async method without running it, consider the following three concepts:

  • A Func object represents an unstarted computation.
  • A new Lazy object represents an unstarted computation that will run once.
  • A call to an async method represents an eventual computation.

Each of these concepts is distinct, but they can be composed with one another into combined representations. Our GetResult<T>() call is an example of the third concept. It will continue execution when its result is ready, either via await or ContinueWith . In our case, we don't even want to start that execution. Instead, we need to combined the first and third concepts into one representation. That is, we need an unstarted computation ( Func ) that, when it finally does start ( async method call), will eventually complete.

Putting this insight into practice, we solve our problem by wrapping GetResult<T>() 's body in a Func .

Func<Task<T>> GetFactory()
{
    return async () =>
    {
        DoRightAway();
        await DoLater();
        return DoEvenLater();
    }
}

We call this new method to get the function:

Func<Task<T>> function = GetFactory();

Congratulations! We got the task without running it! Well, sort of. We have a function that wraps the (yet to be created) task. When ready to create and run the task, then we invoke the function:

Task<T> task = function();

We ensure the task is completed and get its result:

T result = await task;

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM