如何使用async/await在两个等待点上实现任务 [英] How to implement tasks with two awaiting points, using async/await

查看:71
本文介绍了如何使用async/await在两个等待点上实现任务的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一些异步操作,它们由两个不同的阶段组成.最初,我想await他们直到他们的第一阶段完成,然后再await他们直到他们的最后完成.这是这些操作的简化版本:

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

为达到此要求,我想到了将这两个阶段的操作表示为嵌套任务:Task<Task<string>>.这将允许我最初await是外部任务,后来是await是外部任务的结果,也就是内部任务.这是我目前最好的实现此想法的尝试:

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

我喜欢这个解决方案的地方是它可以工作并且可读性很强,因为它不需要像SemaphoreSlimTaskCompletionSource这样的任何特殊同步原语.我不喜欢的是第二阶段是在ThreadPool上下文中执行的,而不是最初的 SynchronizationContext .有什么方法可以使它从头到尾使用当前的SynchronizationContext,而不会使其过于复杂?

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?

我应该再进行一次失败的尝试.将 Task.Run 替换为本地异步功能不起作用,因为出于某些原因,Console.WriteLine($"Stage 2 Started")行是在第一阶段而不是第二阶段的一部分执行的.

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

推荐答案

我假设您想在第一阶段完成后执行某些操作.
您可以将操作作为参数传递给函数.

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

更新后
需求告诉我们TwoStages方法的使用者确实知道操作具有两个阶段,并且该使用者希望在每个阶段之间执行一些操作.
因此,我们需要将每个状态的任务暴露给使用者.
如果将TwoStages方法包装在类中,则可以为其使用方提供更多详细信息.
无论如何,我们都是用面向对象的编程语言编写代码的;)

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

用法

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

用法

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

这篇关于如何使用async/await在两个等待点上实现任务的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆