2013-07-09 39 views
2

我有以下简单的控制台应用程序:异步/ AWAIT线程化的好奇心

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的等待中恢复执行时,我们就和我们在等待中一样。我曾假设,当我们恢复等待方法的执行时,我们会返回到我们等待的线程或完全在另一个线程中。

有谁知道为什么?我的例子有什么缺陷?

回答

4

此行为是由于优化(这是实施细节)。

具体而言,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捕获的任务调度程序是线程池调度程序(因此与延续兼容),因此它将直接执行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)是未记录的实现细节,未来可能会更改。

+0

我有一个与您的代码示例稍有关联的问题。 Task.Run是否使用System.Threading.ThreadPool或Windows.System.Threading.ThreadPool?如果是后者,并且您的处理程序使用RunAsync方法进行排队,则根据http://msdn.microsoft.com/en-us/library/windowsphone/develop/jj248672.aspx,处理程序不应使用async关键字。那是对的吗。或者,这一切是如何工作的? –

+0

我不确定WinRT实现的细节。但是,我可以肯定地说,将'async' lambda传递给'Task.Run'是安全的。 –

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.aspx,您可以将其传递至Task.ContinueWith()。)