2

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

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,因此任务在线程池上运行。但令我惊讶的是,当从 await in 恢复执行时DoSomething,我们在 await 内部的同一线程上。我曾假设当我们继续执行 awaiting 方法时,我们要么返回到我们等待的线程,要么完全在另一个线程上。

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

4

2 回答 2

4

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

await具体来说,通过使用TaskContinuationOptions.ExecuteSynchronously标志安排的延续。这在任何地方都没有正式记录,但几个月前我确实遇到了这个问题,并把它写在了我的博客上

Stephen Toub 有一篇博客文章,它是关于ExecuteSynchronously实际工作原理的最佳文档。重要的一点是,ExecuteSynchronously如果该延续的任务调度程序与当前线程不兼容,则实际上不会同步执行。

正如您所指出的,控制台应用程序没有,因此将使用SynchronizationContext调度的任务延续(在这种情况下是线程池任务调度程序)。awaitTaskScheduler.CurrentTaskScheduler.Default

当您通过 启动另一个任务时Task.Run,您将在线程池上显式执行它。因此,当它到达其方法的末尾时,它会完成其返回的任务,从而导致继续执行(同步)。由于捕获的任务调度await器是线程池调度器(因此与延续兼容),它只会直接执行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得到结果才能真正做任何事情。

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

于 2013-07-10T00:55:23.453 回答
1

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

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

于 2013-07-09T23:07:40.427 回答