【问题标题】:async/await thread transition curiosity异步/等待线程转换好奇心
【发布时间】:2013-07-09 22:52:11
【问题描述】:

我有以下简单的控制台应用程序:

class Program
{
    private static int times = 0;

    static void Main(string[] args)
    {
        Console.WriteLine("Start {0}", Thread.CurrentThread.ManagedThreadId);

        var task = DoSomething();
        task.Wait();

        Console.WriteLine("End {0}", Thread.CurrentThread.ManagedThreadId);

        Console.ReadLine();
    }

    static async Task<bool> DoSomething()
    {
        times++;

        if (times >= 3)
        {
            return true;
        }

        Console.WriteLine("DoSomething-1 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
        await Task.Run(() =>
        {
            Console.WriteLine("DoSomething-1 sleep {0}", Thread.CurrentThread.ManagedThreadId);
            Task.Yield();
        });
        Console.WriteLine("DoSomething-1 awake {0}", Thread.CurrentThread.ManagedThreadId);

        Console.WriteLine("DoSomething-2 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
        await Task.Run(() =>
        {
            Console.WriteLine("DoSomething-2 sleep {0}", Thread.CurrentThread.ManagedThreadId);
            Task.Yield();
        });
        Console.WriteLine("DoSomething-2 awake {0}", Thread.CurrentThread.ManagedThreadId);

        bool b = await DoSomething();
        return b;
    }
}

输出

Start 1
DoSomething-1 sleeping 1
DoSomething-1 sleep 3
DoSomething-1 awake 4
DoSomething-2 sleeping 4
DoSomething-2 sleep 4
DoSomething-2 awake 4
DoSomething-1 sleeping 4
DoSomething-1 sleep 3
DoSomething-1 awake 3
DoSomething-2 sleeping 3
DoSomething-2 sleep 3
DoSomething-2 awake 3
End 1

我知道控制台应用程序不提供 SynchronizationContext,因此任务在线程池上运行。但令我惊讶的是,当从DoSomething 中的等待恢复执行时,我们与等待中的线程处于同一线程上。我曾假设当我们继续执行 awaiting 方法时,我们要么返回到等待的线程,要么完全在另一个线程上。

有人知道为什么吗?我的例子在某些方面有缺陷吗?

【问题讨论】:

    标签: task-parallel-library async-await conceptual


    【解决方案1】:

    这种行为是由于优化(这是一个实现细节)。

    具体来说,await 安排的延续使用TaskContinuationOptions.ExecuteSynchronously 标志。这在任何地方都没有正式记录,但几个月前我确实遇到过这个问题,wrote it up on my blog

    Stephen Toub 有一个blog post that is the best documentation on how ExecuteSynchronously actually works。重要的一点是ExecuteSynchronously 不会真正同步执行,如果该延续的任务调度程序与当前线程不兼容。

    正如您所指出的,控制台应用程序没有SynchronizationContext,因此由await 调度的任务延续将使用TaskScheduler.Current(在本例中为TaskScheduler.Default,线程池任务调度程序)。

    当您通过Task.Run 启动另一个任务时,您是在线程池上显式执行它。因此,当它到达其方法的末尾时,它会完成其返回的任务,从而导致继续执行(同步)。由于await捕获的任务调度器是线程池调度器(因此与continuation兼容),它只会直接执行DoSomething的下一部分。

    请注意,这里存在竞争条件。 DoSomething 的下一部分只有在已经作为Task.Run 返回的任务的延续附加到它时才会同步执行。在我的机器上,第一个Task.Run 将在另一个线程上恢复DoSomething,因为在Task.Run 委托完成时没有附加延续;第二个Task.Run 确实在同一线程上恢复DoSomething

    所以我修改了代码,使其更具确定性;这段代码:

    static Task DoSomething()
    {
        return Task.Run(async () =>
        {
            Console.WriteLine("DoSomething-1 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
            await Task.Run(() =>
            {
                Console.WriteLine("DoSomething-1 sleep {0}", Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(100);
            });
            Console.WriteLine("DoSomething-1 awake {0}", Thread.CurrentThread.ManagedThreadId);
    
            Console.WriteLine("DoSomething-2 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
            var task = Task.Run(() =>
            {
                Console.WriteLine("DoSomething-2 sleep {0}", Thread.CurrentThread.ManagedThreadId);
            });
            Thread.Sleep(100);
            await task;
            Console.WriteLine("DoSomething-2 awake {0}", Thread.CurrentThread.ManagedThreadId);
        });
    }
    

    (在我的机器上)显示了竞争条件的两种可能性:

    Start 8
    DoSomething-1 sleeping 9
    DoSomething-1 sleep 10
    DoSomething-1 awake 10
    DoSomething-2 sleeping 10
    DoSomething-2 sleep 11
    DoSomething-2 awake 10
    End 8
    

    顺便说一句,您对Task.Yield 的使用不正确;你必须await 结果才能真正做任何事情。

    请注意,此行为(await 使用 ExecuteSynchronously)是未记录的实现细节,将来可能会更改。

    【讨论】:

    • 我对您的代码示例有一个稍微相关的问题。 Task.Run 这里使用 System.Threading.ThreadPool 还是 Windows.System.Threading.ThreadPool?如果是后者并且您的处理程序使用 RunAsync 方法排队,根据msdn.microsoft.com/en-us/library/windowsphone/develop/…,您的处理程序不应使用 async 关键字。那是对的吗。或者,这一切是如何运作的?
    • 我不确定 WinRT 实现的细节。但是,我可以肯定地说,将 async lambda 传递给 Task.Run 是安全的。
    【解决方案2】:

    当您没有指定要使用哪个调度程序时,您会随心所欲地决定在何处/如何运行您的任务。 await 真正所做的只是将所有代码跟随等待的任务放入等待任务完成后运行的延续任务。在很多情况下,调度器会说“嘿,我刚刚在线程 X 上完成了一个任务,并且还有一个延续任务……既然线程 X 已经完成,我就将它重新用于延续!”这正是您所看到的行为。 (详情请参阅http://msdn.microsoft.com/en-US/library/vstudio/hh156528.aspx。)

    如果您手动创建延续(而不是让await 为您创建),您可以更好地控制延续的运行方式和位置。 (有关可以传递给Task.ContinueWith() 的延续选项,请参阅http://msdn.microsoft.com/en-us/library/system.threading.tasks.taskcontinuationoptions.aspx。)

    【讨论】:

      猜你喜欢
      • 2020-07-08
      • 2013-02-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-13
      • 2018-10-01
      相关资源
      最近更新 更多