How to implement tasks with two awaiting points, using async/await

和自甴很熟 提交于 2020-06-16 17:48:15

问题


I have some asynchronous operations that consist of two distinct stages. Initially I want to await them until the completion of their first stage, and later await them until their final completion. Here is a simplified version of these operations:

async Task<string> TwoStagesAsync()
{
    Console.WriteLine($"Stage 1 Started");
    await Task.Delay(1000); // Simulate an I/O operation
    bool resultOfStage1 = true;
    Console.WriteLine($"Stage 1 Finished");
    if (!resultOfStage1) return null;
    /* Stage separator */
    Console.WriteLine($"Stage 2 Started");
    await Task.Delay(1000); // Simulate an I/O operation
    Console.WriteLine($"Stage 2 Finished");
    return "Hello!";
}

To achieve this requirement I had the idea of representing these two-stage operations as nested tasks: Task<Task<string>>. This would allow me to await initially the outer task, and later await the result of the outer task, which would be the inner task. This is my currently best attempt to implement this idea:

async Task<Task<string>> TwoStagesNestedAsync_A() // Problematic
{
    Console.WriteLine($"Stage 1 Started");
    await Task.Delay(1000); // Simulate an I/O operation
    bool resultOfStage1 = true;
    Console.WriteLine($"Stage 1 Finished");
    if (!resultOfStage1) return Task.FromResult((string)null);
    /* Stage separator */
    return Task.Run(async () =>
    {
        Console.WriteLine($"Stage 2 Started");
        await Task.Delay(1000); // Simulate an I/O operation
        Console.WriteLine($"Stage 2 Finished");
        return "Hello!";
    });
}

What I like to this solution is that it works and it is quite readable, since it doesn't require any special synchronization primitives like SemaphoreSlim or TaskCompletionSource. What I don't like is that the second stage is executed in the ThreadPool context instead of the initial SynchronizationContext. Is there any way to make it use the current SynchronizationContext from start to finish, without complicating it too much?

I should include one more of my failed attempts. Replacing the Task.Run with a local async function doesn't work, because for some reason the line Console.WriteLine($"Stage 2 Started") is executed as part of the first stage, instead of the second stage.

async Task<Task<string>> TwoStagesNestedAsync_B() // Problematic
{
    Console.WriteLine($"Stage 1 Started");
    await Task.Delay(1000); // Simulate an I/O operation
    bool resultOfStage1 = true;
    Console.WriteLine($"Stage 1 Finished");
    if (!resultOfStage1) return Task.FromResult((string)null);
    return SecondStageAsync();

    async Task<string> SecondStageAsync()
    {
        Console.WriteLine($"Stage 2 Started");
        await Task.Delay(1000); // Simulate an I/O operation
        Console.WriteLine($"Stage 2 Finished");
        return "Hello!";
    }
}

Update: Here is an example of consuming an asynchronous operation that consists of two stages:

Task<Task<string>>[] operations = Enumerable.Range(1, 10)
    .Select(_ => TwoStagesNestedAsync_A())
    .ToArray();

/* Do something else before awaiting Stage 1 */

Task<string>[] innerTasks = await Task.WhenAll(operations);
Console.WriteLine($"Stage 1 is now complete");

/* Do something else before awaiting Stage 2 */

string[] results = await Task.WhenAll(innerTasks);
Console.WriteLine($"Stage 2 is now complete");

回答1:


I assume you want to execute something when first stage is complete.
You can pass an action as parameter to the function.

public async Task<string> TwoStagesAsync(Func<Task> injectedAction)
{
    await ExecuteStageOne();

    // Execute without "stopping" second stage
    var injectedTask = injectedAction.Invoke();

    if (somethingFailed) return null;
    /* Stage separator */

    await ExecuteStageTwo();

    await injectedTask; // Make sure it completes without errors
    return "Hello!";
}

After update
Requirements tell us that consumer of the TwoStages method do know that operation has two stages and this consumer want execute some action between every stage.
So we need to expose tasks of every state to the consumer.
If you wrap TwoStages method within a class, you can expose more details for its consumers.
We write code in object-oriented programming language anyway, isn't it ;)

public class TwoStageOperation
{
    public TwoStageOperation() { }

    public async Task ExecuteFirstStage()
    {
        Console.WriteLine($"Stage 1 Started");
        await Task.Delay(1000);
        Console.WriteLine($"Stage 1 Finished");
    }

    public async Task<string> ExecuteLastStage()
    {
        Console.WriteLine($"Stage 2 Started");
        await Task.Delay(1000);
        Console.WriteLine($"Stage 2 Finished");

        return "Hello";
    }
}

Usage

var operations = Enumerable.Range(1, 10)
    .Select(_ => new TwoStageOperation())
    .ToArray();

/* Do something else before awaiting Stage 1 */


await Task.WhenAll(operations.Select(op => op.ExecuteFirstStage());
Console.WriteLine($"Stage 1 is now complete");

/* Do something else before awaiting Stage 2 */

string[] results = await Task.WhenAll(operations.Select(op => op.ExecuteLastStage());
Console.WriteLine($"Stage 2 is now complete");

In case operations has different implementations, you can introduce an interface and have different implementations

public interface ITwoStageOperation
{
    Task ExecuteFirstStage();
    Task<string> ExecuteLastStage();
}

var operations = new ITwoStageOperation[]
{
    new LandTwoStageOperation(),
    new OceanTwoStageOperation(),
    new AirTwoStageOperation(),
};

Alternative approach
Which I think you will prefer more, because you were very close to it :), would be to return a function as result of first stage

public async Task<Func<Task<string>>> TwoStagesAsync()
{
    await ExecuteStageOne();

    Func<Task<string>> lastStage = async () =>
    {
         await Task.Delay(1000);
         return "Hello";
    };

    return lastStage;
}

Usage

var firstStages = Enumerable.Range(1, 10)
    .Select(_ => TwoStagesAsync())
    .ToArray();

/* Do something else before awaiting Stage 1 */

var lastStages = await Task.WhenAll(firstStages);
Console.WriteLine($"Stage 1 is now complete");

/* Do something else before awaiting Stage 2 */

string[] results = await Task.WhenAll(lastStages.Select(s => s.Invoke());
Console.WriteLine($"Stage 2 is now complete");



回答2:


Try this.

public static async Task<bool> FirstStageAsync() // Problematic
    {
        Console.WriteLine($"Stage 1 Started");
        await Task.Delay(1000); // Simulate an I/O operation
        bool resultOfStage1 = true;
        Console.WriteLine($"Stage 1 Finished");
        return await Task.FromResult(resultOfStage1);
    }

    public static async Task<string> SecondStageAsync()
    {
        Console.WriteLine($"Stage 2 Started");
        await Task.Delay(1000); // Simulate an I/O operation
        Console.WriteLine($"Stage 2 Finished");
        return "Hello!";
    }

Then you can call it by:

var task = FirstStageAsync().ContinueWith(async d =>
        {
            if (d.IsCompleted)
            {
                var resultOfStage1 = await d;
                if (resultOfStage1)
                {
                    var task2 = SecondStageAsync();
                    task2.Wait();
                }
            }
        });

        task.Wait();

Is that you want to achieve?

Be careful with deadlock. Good Luck!



来源:https://stackoverflow.com/questions/61981628/how-to-implement-tasks-with-two-awaiting-points-using-async-await

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!