等待运算符没有像我预期的那样等待

2024-04-02

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

代码为测试方法:

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

我期望此测试的输出是它会向我显示:

DelayExecute 1 之外的 sw.ElapsedMilliseconds:...

行动起来!”

DelayExecute 内的 sw.ElapsedMilliseconds:...

DelayExecute 2 之外的 sw.ElapsedMilliseconds:

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

sw.ElapsedMilliseconds 超出 DelayExecute 1: 0

sw.ElapsedMilliseconds 超出 DelayExecute 2: 30

跑行动!

DelayExecute 内的 sw.ElapsedMilliseconds:1015

On this 博客文章 http://blog.stephencleary.com/2012/02/async-and-await.html I read:

我喜欢将“等待”视为“异步等待”。也就是说,异步方法会暂停,直到等待完成(因此它会等待),但实际线程并未被阻塞(因此它是异步的)。

这似乎符合我的预期,那么这是怎么回事,我的错误在哪里?

代码为延迟执行器:

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

Update:

按照建议,我在我的内部修改了这一行DelayExecute

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

into

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

为了这个工作,我需要将签名更改为这个

public async Task<Task> DelayExecute(Action func)

所以我的新定义是这样的:

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

然而现在我的行​​为和以前一样。有什么方法可以实现我想要使用我的设计做的事情吗?或者说它有根本性的缺陷吗?

顺便说一下,我也尝试过await await delayTask;在我的测试中DelayExecuteTest,但这给了我错误

无法等待“无效”

这是更新的测试方法DelayExecuteTest, which does not编译:

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

这里发生了一些事情,所以我将首先发布一个简单的答案,让我们看看它是否足够。

我们将一个任务包装在一个任务中,这需要双重等待。否则,您只是等待内部任务到达其第一个返回点,该返回点将(可以)位于第一个await.

让我们看看你的DelayExecute方法更详细。让我从更改方法的返回类型开始,我们将看到这如何改变我们的观点。返回类型的更改只是一个说明。您正在返回一个Task现在这是非通用的,实际上你正在返回一个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,但是如果您阅读了问题的其余部分,这在很大程度上是无关紧要的)

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

让我们先解释一个更简单的例子,我在中测试了这个LINQPad http://linqpad.net:

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

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

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

那么为什么会这样呢?

嗯,你的StartNew返回一个Task<Task>。这不是一个现在将等待内部任务完成的任务,它等待内部任务完成return,它首先会(可以)做await.

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

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执行

这里没什么神奇的

2. 我们启动一个新任务Task.Factory.StartNew并等待它。

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

内部任务的工作是not执行all该委托中的代码。内部任务的工作是产生另一个任务.

这个很重要!

3. 内部任务现在开始执行

它本质上会执行委托中的所有代码,包括调用Task.Delay,它返回一个Task.

4. 内部任务首先到达await

由于这尚未完成(它只会在大约 500 毫秒后完成),因此该委托现在返回.

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

这个很重要!

6. 我们的外部任务现在继续

由于内部任务返回,外部任务现在可以继续。调用的结果await Task.Factory.StartNew又是另一项任务,但是这项任务只能靠自己解决了.

5. 内部任务,大约持续500ms后

现在是在外部任务已经继续执行之后,可能已经完成执行。


所以总而言之,你的代码“按预期”执行,只是不是这样you预期的。

有多种方法可以修复此代码,我只是重写您的DelayExecute方法以最简单的方式做你想做的事:

更改这一行:

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

进入这个:

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

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

与我类似的修复LINQPad上面的例子就是简单地改变这一行:

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

To this:

await await Task.Factory.StartNew(async () =>
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

等待运算符没有像我预期的那样等待 的相关文章