当线程在while循环内等待任务时,会发生什么?


10

在处理了C#的async / await模式一段时间之后,我突然意识到我不太了解如何解释以下代码中发生的情况:

async void MyThread()
{
    while (!_quit)
    {
        await GetWorkAsync();
    }
}

GetWorkAsync()假定返回一个等待对象Task,该等待对象在执行继续操作时可能会或可能不会导致线程切换。

如果等待不在循环内,我不会感到困惑。我自然希望该方法的其余部分(即延续)可以在另一个线程上执行,这很好。

但是,在一个循环中,“其余方法”的概念让我有些迷惑。

如果线程是在连续状态下打开的,而线程没有被打开,那么“其余的循环”会怎样?在哪个线程上执行循环的下一个迭代?

我的观察表明(未最终验证),每个迭代都在同一线程(原始线程)上开始,而延续在另一个线程上执行。真的可以吗?如果是,那么这是否是某种程度的意外并行性,需要解决GetWorkAsync方法相对于线程安全性的问题?

更新:我的问题并非如某些人所建议的重复。该while (!_quit) { ... }代码模式仅仅是我的实际代码的简化。实际上,我的线程是一个长寿命的循环,它以固定的时间间隔(默认为每5秒)处理其工作项的输入队列。实际的退出条件检查也不是示例代码所建议的简单的字段检查,而是事件句柄检查。



1
另请参见如何在.NET中让出并等待实现控制流?有关如何将它们连接在一起的一些重要信息。
John Wu,

@John Wu:我还没有看到SO线程。那里有很多有趣的信息块。谢谢!
aoven

Answers:


6

您实际上可以在Try Roslyn上进行检查。您的await方法将重新写入void IAsyncStateMachine.MoveNext()生成的异步类中。

您将看到的是这样的:

            if (this.state != 0)
                goto label_2;
            //set up the state machine here
            label_1:
            taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter);
            label_2:
            if (!OuterClass._quit)
            {
               taskAwaiter = GetWorkAsync().GetAwaiter();
               //state machine stuff here
            }
            goto label_1;

基本上,您在哪个线程上都没有关系;通过使用等效的if / goto结构替换循环,状态机可以正确恢复。

话虽这么说,异步方法不一定在不同的线程上执行。请参阅Eric Lippert的解释“这不是魔术”,以解释如何async/await只使用一个线程。


2
我似乎低估了编译器对异步代码进行重写的程度。本质上,重写后没有“循环”!那是我所缺少的部分。太棒了,也感谢您的“尝试罗斯林”链接!
aoven

GOTO是原始的循环构造。我们不要忘记这一点。

2

首先,Servy在一个类似问题的答案中编写了一些代码,该答案基于此:

/programming/22049339/how-to-create-a-cancellable-task-loop

Servy的答案包括ContinueWith()使用TPL构造的类似循环,而没有显式使用asyncand await关键字。因此,要回答您的问题,请考虑使用以下方法展开循环时代码的外观ContinueWith()

    private static Task GetWorkWhileNotQuit()
    {
        var tcs = new TaskCompletionSource<bool>();

        Task previous = Task.FromResult(_quit);
        Action<Task> continuation = null;
        continuation = t =>
        {
            if (!_quit)
            {
                previous = previous.ContinueWith(_ => GetWorkAsync())
                    .Unwrap()
                    .ContinueWith(_ => previous.ContinueWith(continuation));
            }
            else
            {
                tcs.SetResult(_quit);
            }
        };
        previous.ContinueWith(continuation);
        return tcs.Task;
    }

这需要花一些时间来解决,但是总之:

  • continuation表示“当前迭代”的关闭
  • previous表示Task包含“上一个迭代”的状态(即,它知道“迭代”何时结束,并用于开始下一个迭代。)
  • 假设GetWorkAsync()返回Task,则意味着ContinueWith(_ => GetWorkAsync())将返回,Task<Task>因此调用Unwrap()以获得“内部任务”(即的实际结果GetWorkAsync())。

所以:

  1. 最初没有先前的迭代,因此仅为其分配了一个值Task.FromResult(_quit) -其状态开始为Task.Completed == true
  2. continuation使用运行在第一次previous.ContinueWith(continuation)
  3. continuation封闭的更新previous,以反映的完成状态_ => GetWorkAsync()
  4. _ => GetWorkAsync()完成后,其“继续” _previous.ContinueWith(continuation)-即调用continuation再次拉姆达
    • 显然,在这一点上,previous状态已更新,_ => GetWorkAsync()因此返回continuationGetWorkAsync()将调用lambda 。

continuation拉姆达总是检查的状态,_quit所以,如果_quit == false再没有更多的延续,以及TaskCompletionSource被设置为值_quit,一切完成。

至于您对在另一个线程中执行连续性的观察,这不是async/ await关键字将为您执行的操作,根据此博客“任务是(仍然)不是线程并且异步不是并行的”。- https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

我建议确实值得仔细研究一下GetWorkAsync()有关线程和线程安全性的方法。如果您的诊断显示由于重复的异步/等待代码而导致它在不同的线程上执行,则该方法内或与之相关的某些事物必须导致在其他位置创建新线程。(如果这是意外的,也许在.ConfigureAwait某个地方?)


2
我显示的代码非常简化。在GetWorkAsync()内部,还有多个等待。他们中的一些人访问数据库和网络,这意味着真正的I / O。据我了解,线程切换是此类等待的自然结果(尽管不是必需的),因为初始线程未建立任何用于控制继续执行位置的同步上下文。因此,它们在线程池线程上执行。我的推理错了吗?
aoven's

@aoven好点-我没考虑过不同的点SynchronizationContext-这当然很重要,因为.ContinueWith()使用SynchronizationContext调度了延续;它确实可以解释您是await在ThreadPool线程还是在ASP.NET线程上调用的行为。在这些情况下,可以肯定地将延续发送到另一个线程。另一方面,调用await单线程上下文(例如WPF Dispatcher或Winforms上下文)应足以确保在原始线程上进行延续。线程
Ben Cottrell
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.