等待操作员没有像我预期的那样等待 [英] The await operator is not waiting like I expected

查看:71
本文介绍了等待操作员没有像我预期的那样等待的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在研究类DelayedExecutor,该类将使用async和await语句将传递给DelayExecute方法的Action的执行延迟特定时间timeout(请参见下面的代码).如果需要,我还希望能够在timeout间隔内中止执行.我编写了一个小型测试来测试其行为,如下所示:

I am working on a class DelayedExecutor that will delay the execution of an Action passed to its DelayExecute method by a certain time timeout (see code below) using the async and await statements. I also want to be able to abort the execution within the timeout interval if needed. I have written a small test to test its behavior like this:

测试方法的代码:

    [TestMethod]
    public async Task DelayExecuteTest()
    {
        int timeout = 1000;
        var delayExecutor = new DelayedExecutor(timeout);
        Action func = new Action(() => Debug.WriteLine("Ran function!"));
        var sw = Stopwatch.StartNew();
        Debug.WriteLine("sw.ElapsedMilliseconds 1: " + sw.ElapsedMilliseconds);
        Task delayTask = delayExecutor.DelayExecute(func);
        await delayTask;
        Debug.WriteLine("sw.ElapsedMilliseconds 2: " + sw.ElapsedMilliseconds);
        Thread.Sleep(1000);
    }
}

此测试中我期望的输出是它将显示给我:

The output I expected from this test was that it would show me:

DelayExecute 1以外的sw.ElapsedMilliseconds ... ...

sw.ElapsedMilliseconds outside DelayExecute 1: ...

Ran Action!"

Ran Action!"

DelayExecute内的sw.ElapsedMilliseconds:...

sw.ElapsedMilliseconds inside DelayExecute: ...

delayExecute 2之外的sw.ElapsedMilliseconds

sw.ElapsedMilliseconds outside DelayExecute 2:

但是我明白了,但不明白为什么:

However I get this and do not understand why:

sw.ElapsedMillisecond超出DelayExecute 1:0

sw.ElapsedMilliseconds outside DelayExecute 1: 0

sw.ElapsedMillisecond超出DelayExecute 2:30

sw.ElapsedMilliseconds outside DelayExecute 2: 30

Ran Action!

Ran Action!

sw.ElapsedMilliseconds内部DelayExecute:1015

sw.ElapsedMilliseconds inside DelayExecute: 1015

在此博客文章中,我读到:

我喜欢将等待"视为异步等待".也就是说,async方法会暂停,直到awaitable完成(因此它会等待),但是实际线程不会被阻塞(因此它是异步的).

I like to think of "await" as an "asynchronous wait". That is to say, the async method pauses until the awaitable is complete (so it waits), but the actual thread is not blocked (so it’s asynchronous).

这似乎符合我的期望,所以这里发生了什么,我的错误在哪里?

This seems to be inline with my expectation, so what is going on here and where is my error?

DelayedExecutor 的代码:

public class DelayedExecutor
{
    private int timeout;

    private Task currentTask;
    private CancellationToken cancellationToken;
    private CancellationTokenSource tokenSource;

    public DelayedExecutor(int timeout)
    {
        this.timeout = timeout;
        tokenSource = new CancellationTokenSource();
    }

    public void AbortCurrentTask()
    {
        if (currentTask != null)
        {
            if (!currentTask.IsCompleted)
            {
                tokenSource.Cancel();
            }
        }
    }

    public Task DelayExecute(Action func)
    {
        AbortCurrentTask();

        tokenSource = new CancellationTokenSource();
        cancellationToken = tokenSource.Token;

        return currentTask = Task.Factory.StartNew(async () =>
            {
                var sw = Stopwatch.StartNew();
                await Task.Delay(timeout, cancellationToken);
                func();
                Debug.WriteLine("sw.ElapsedMilliseconds inside DelayExecute: " + sw.ElapsedMilliseconds);
            });
    }
}

更新:

根据建议,我在DelayExecute

return currentTask = Task.Factory.StartNew(async () =>

进入

return currentTask = await Task.Factory.StartNew(async () =>

要使其正常工作,我需要将签名更改为此

For this to work I needed to change the signature into this

public async Task<Task> DelayExecute(Action func)

所以我的新定义是这样:

so that my new definition is this:

public async Task<Task> DelayExecute(Action func)
{
    AbortCurrentTask();

    tokenSource = new CancellationTokenSource();
    cancellationToken = tokenSource.Token;

    return currentTask = await Task.Factory.StartNew(async () =>
        {
            var sw = Stopwatch.StartNew();
            await Task.Delay(timeout, cancellationToken);
            func();
            Debug.WriteLine("sw.ElapsedMilliseconds inside DelayExecute: " + sw.ElapsedMilliseconds);
        });
}

但是现在我的行​​为与以前相同.是否有某种方法可以使用我的设计来实现我想要做的事情.还是从根本上有缺陷?

However now I have the same behavior as before. Is there some way achieve what I am trying to do using my design. Or is it fundamentally flawed?

顺便说一句,我也尝试将await await delayTask;放入测试DelayExecuteTest中,但这给了我错误

By the way, I also tried putting await await delayTask; inside my test DelayExecuteTest, but this gives me the error

无法等待空"

Cannot await 'void'

这是更新的测试方法DelayExecuteTest不会编译:

This is the updated test-method DelayExecuteTest, which does not compile:

public async Task DelayExecuteTest()
{
    int timeout = 1000;
    var delayExecutor = new DelayedExecutor(timeout);
    Action func = new Action(() => Debug.WriteLine("Ran Action!"));
    var sw = Stopwatch.StartNew();
    Debug.WriteLine("sw.ElapsedMilliseconds outside DelayExecute 1: " + sw.ElapsedMilliseconds);
    Task delayTask = delayExecutor.DelayExecute(func);
    await await delayTask;
    Debug.WriteLine("sw.ElapsedMilliseconds outside DelayExecute 2: " + sw.ElapsedMilliseconds);
    Thread.Sleep(1000);
}

推荐答案

这里有一些问题,因此我将首先发布一个简单的答案,让我们看看它是否足够.

There's a bit going on here so I'm going to post a simple answer to begin with, let's see if it suffices.

您已将任务包装在任务中,这需要两次等待.否则,您仅在等待内部任务到达其第一个返回点,该返回点将(可能)在第一个await处.

You'we wrapped a task inside a task, this needs a double await. Otherwise you're only waiting for the inner task to reach its first return point, which will (can) be at the first await.

让我们更详细地了解您的DelayExecute方法.让我从更改方法的返回类型开始,我们将看到这如何改变我们的观点.返回类型的更改只是一种澄清.您正在返回的是非通用的Task,实际上,您正在返回的是Task<Task>.

Let's look at your DelayExecute method in more detail. Let me begin with changing the return type of the method and we'll see how this changes our perspective. The change of return type is just a clarification. You're returning a Task that is nongeneric right now, in reality you're returning a Task<Task>.

public Task<Task> DelayExecute(Action func)
{
    AbortCurrentTask();

    tokenSource = new CancellationTokenSource();
    cancellationToken = tokenSource.Token;

    return currentTask = Task.Factory.StartNew(async () =>
        {
            var sw = Stopwatch.StartNew();
            await Task.Delay(timeout, cancellationToken);
            func();
            Debug.WriteLine("sw.ElapsedMilliseconds inside DelayExecute: " + sw.ElapsedMilliseconds);
        });
}

(请注意,由于currentTask的类型也为Task,因此不会编译,但是如果您阅读其余的问题,这在很大程度上是不相关的)

(note that this will not compile since currentTask is also of type Task, but this is largely irrelevant if you read the rest of the question)

好,那么这里会发生什么?

OK, so what happens here?

让我们先解释一个简单的示例,我在 LINQPad 中进行了测试:

Let's explain a simpler example first, I tested this in LINQPad:

async Task Main()
{
    Console.WriteLine("before await startnew");
    await Task.Factory.StartNew(async () =>
    {
        Console.WriteLine("before await delay");
        await Task.Delay(500);
        Console.WriteLine("after await delay");
    });
    Console.WriteLine("after await startnew");
}

执行此操作时,我得到以下输出:

When executing this I get this output:

before await startnew
before await delay
after await startnew
after await delay              -- this comes roughly half a second after previous line

那为什么呢?

好吧,您的StartNew返回一个Task<Task>.这不是现在要等待内部任务完成的任务,而是等待内部任务返回的任务,它将在第一个await上完成(可以).

Well, your StartNew returns a Task<Task>. This is not a task that now will wait for the inner task to complete, it waits for the inner task to return, which it will (can) do at its first await.

因此,我们通过给有趣的行编号,然后解释事情发生的顺序,来查看其完整的执行路径:

So let's see the full execution path of this by numbering the interesting lines and then explaining the order in which things happen:

async Task Main()
{
    Console.WriteLine("before await startnew");           1
    await Task.Factory.StartNew(async () =>               2
    {
        Console.WriteLine("before await delay");          3
        await Task.Delay(500);                            4
        Console.WriteLine("after await delay");           5
    });
    Console.WriteLine("after await startnew");            6
}

1.第一个Console.WriteLine执行

这里没有神奇的东西

1. The first Console.WriteLine executes

nothing magical here

这里我们的 main 方法现在可以返回,它将返回一个任务,该任务将在内部任务完成后继续执行.

Here our main method can now return, it will return a task that will continue once the inner task has completed.

内部任务的工作不是不是,而是要执行 all 该委托中的代码.内部任务的工作是产生另一个任务.

The job of the inner task is not to execute all the code in that delegate. The job of the inner task is to produce yet another task.

这很重要!

它将实质上执行委托中的所有代码,直至调用Task.Delay的所有代码,该调用将返回Task.

It will essentially execute all the code in the delegate up to and including the call to Task.Delay, which returns a Task.

由于此操作尚未完成(只会在大约500毫秒后完成),因此该代表现在返回.

Since this will not already have completed (it will only complete roughly 500ms later), this delegate now returns.

基本上,await <this task we got from Task.Delay>语句将创建一个延续,然后返回.

Basically, the await <this task we got from Task.Delay> statement will create a continuation and then return.

这很重要!

由于内部任务已返回,因此外部任务现在可以继续.调用await Task.Factory.StartNew的结果是另一项任务,但该任务仅由其自己解决.

Since the inner task returned, the outer task can now continue. The result of calling await Task.Factory.StartNew is yet another task but this task is just left to fend for itself.

现在这是外部任务已经继续执行,可能完成执行之后的结果.

This is now after the outer task has already continued, potentially finished executing.

因此,总而言之,您的代码按预期"执行,而不是您按 预期的方式执行.

So in conclusion your code executes "as expected", just not the way you expected.

有几种方法可以修复此代码,我只是将您的DelayExecute方法重写为最简单的方法来完成您想做的事情:

There are several ways to fix this code, I'm simply going to rewrite your DelayExecute method to the simplest way to do what you want to do:

更改此行:

    return currentTask = Task.Factory.StartNew(async () =>

对此:

    return currentTask = await Task.Factory.StartNew(async () =>

这将使内部任务启动秒表并到达第一个等待状态,然后完全返回DelayExecute.

This will let the inner task start your stopwatch and reach the first await before returning all the way out of DelayExecute.

与我上面的 LINQPad 示例类似的修复方法是简单地更改此行:

The similar fix to my LINQPad example above would be to simply change this line:

await Task.Factory.StartNew(async () =>

对此:

await await Task.Factory.StartNew(async () =>

这篇关于等待操作员没有像我预期的那样等待的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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