我对使用async修饰符进行异步编程并不熟悉。我试图弄清楚如何确保我的控制台应用程序的主要方法实际上是异步运行的。

class Program
{
    static void Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = bs.GetList();
    }
}

public class Bootstrapper {

    public async Task<List<TvChannel>> GetList()
    {
        GetPrograms pro = new GetPrograms();

        return await pro.DownloadTvChannels();
    }
}

我知道这不是从“顶层”异步运行的。由于不可能在Main方法上指定异步修饰符,我如何在Main中异步运行代码?


当前回答

正如你所发现的,在VS11中编译器将不允许async Main方法。在VS2010的Async CTP中,这是允许的(但从不推荐)。

更新,2017-11-30:从Visual Studio 2017更新3(15.3)开始,该语言现在支持异步Main -只要它返回Task或Task<T>。所以你现在可以这样做:

class Program
{
    static async Task Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = await bs.GetList();
    }
}

语义似乎与阻塞主线程的GetAwaiter(). getresult()样式相同。然而,c# 7.1还没有语言规范,所以这只是一个假设。


我最近有一篇关于异步/等待和异步控制台程序的博客文章。以下是介绍帖子中的一些背景信息:

如果“await”看到可等待对象还没有完成,那么它就会异步操作。它告诉可等待对象在方法完成时运行该方法的剩余部分,然后从异步方法返回。当Await将方法的剩余部分传递给可等待对象时,它还将捕获当前上下文。 稍后,当可等待对象完成时,它将执行异步方法的剩余部分(在捕获的上下文中)。

以下是为什么这在带有异步Main的控制台程序中是一个问题:

请记住,在我们的介绍文章中,异步方法将在完成之前返回给它的调用者。这在UI应用程序(方法只返回到UI事件循环)和ASP。NET应用程序(该方法返回线程,但保持请求活动)。这对于控制台程序来说就不太适用了:Main返回到操作系统——所以你的程序退出了。

一个解决方案是提供你自己的上下文——一个异步兼容的控制台程序的“主循环”。

如果你有一台带有Async CTP的机器,你可以使用My Documents\Microsoft Visual Studio Async CTP\Samples(c# Testing) Unit Testing\AsyncTestUtilities中的GeneralThreadAffineContext。或者,你可以从我的Nito使用AsyncContext。AsyncEx NuGet包。

下面是一个使用AsyncContext的例子;GeneralThreadAffineContext几乎有相同的用法:

using Nito.AsyncEx;
class Program
{
    static void Main(string[] args)
    {
        AsyncContext.Run(() => MainAsync(args));
    }

    static async void MainAsync(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = await bs.GetList();
    }
}

或者,你可以阻塞主控制台线程,直到你的异步工作完成:

class Program
{
    static void Main(string[] args)
    {
        MainAsync(args).GetAwaiter().GetResult();
    }

    static async Task MainAsync(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = await bs.GetList();
    }
}

注意使用GetAwaiter().GetResult();这避免了使用Wait()或Result时发生的AggregateException包装。

其他回答

你也可以在不需要外部库的情况下做到这一点,方法如下:

class Program
{
    static void Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var getListTask = bs.GetList(); // returns the Task<List<TvChannel>>

        Task.WaitAll(getListTask); // block while the task completes

        var list = getListTask.Result;
    }
}

当你在调用堆栈的某个地方调用一个函数,试图重新加入当前线程(被卡在等待中)时,为了避免冻结,你需要做以下事情:

class Program
{
    static void Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        List<TvChannel> list = Task.Run((Func<Task<List<TvChannel>>>)bs.GetList).Result;
    }
}

(转换仅用于解决歧义)

不确定这是否是你要找的,但我想等待加载方法。我最终使用main_shows处理程序,并使它异步:

private async void Main_Shown(object sender, EventArgs e)
{
   await myAsyncMethod();
}

c# 7.1(使用vs 2017更新3)引入了async main

你可以这样写:

   static async Task Main(string[] args)
  {
    await ...
  }

有关c# 7系列的更多详细信息,第2部分:异步主

更新:

你可能会得到一个编译错误:

程序不包含适用于入口点的静态'Main'方法

此错误是由于vs2017.3默认配置为c#7.0而不是c#7.1。

您应该显式地修改项目的设置来设置c#7.1特性。

你可以通过以下两种方法设置c#7.1:

方法一:使用项目设置窗口:

打开项目的设置 选择Build选项卡 点击高级按钮 选择所需的版本 如下图所示:

方法2:手动修改.csproj的PropertyGroup

添加这个属性:

    <LangVersion>7.1</LangVersion>

例子:

    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
        <PlatformTarget>AnyCPU</PlatformTarget>
        <DebugSymbols>true</DebugSymbols>
        <DebugType>full</DebugType>
        <Optimize>false</Optimize>
        <OutputPath>bin\Debug\</OutputPath>
        <DefineConstants>DEBUG;TRACE</DefineConstants>
        <ErrorReport>prompt</ErrorReport>
        <WarningLevel>4</WarningLevel>
        <Prefer32Bit>false</Prefer32Bit>
        <LangVersion>7.1</LangVersion>
    </PropertyGroup>    

在MSDN上,Task的文档。Run Method (Action)提供了这个例子,展示了如何从main异步运行一个方法:

using System;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
    public static void Main()
    {
        ShowThreadInfo("Application");

        var t = Task.Run(() => ShowThreadInfo("Task") );
        t.Wait();
    }

    static void ShowThreadInfo(String s)
    {
        Console.WriteLine("{0} Thread ID: {1}",
                          s, Thread.CurrentThread.ManagedThreadId);
    }
}
// The example displays the following output:
//       Application thread ID: 1
//       Task thread ID: 3

请注意下面的例子:

这些例子表明异步任务执行在不同的 线程比主应用程序线程多。

因此,如果您希望任务在主应用程序线程上运行,请参阅@StephenCleary的回答。

关于任务运行的线程,也请注意Stephen对他的回答的评论:

您可以使用简单的Wait或Result,没有任何问题 的。但要注意两者之间有两个重要的区别: 所有异步延续都运行在线程池上,而不是主线程池上 2)任何异常都包装在AggregateException中。

(参见异常处理(任务并行库)了解如何合并异常处理来处理AggregateException。)


最后,从MSDN上的任务文档。延迟方法(TimeSpan),这个例子展示了如何运行一个返回值的异步任务:

using System;
using System.Threading.Tasks;

public class Example
{
    public static void Main()
    {
        var t = Task.Run(async delegate
                {
                    await Task.Delay(TimeSpan.FromSeconds(1.5));
                    return 42;
                });
        t.Wait();
        Console.WriteLine("Task t Status: {0}, Result: {1}",
                          t.Status, t.Result);
    }
}
// The example displays the following output:
//        Task t Status: RanToCompletion, Result: 42

注意,不是传递一个委托给Task。运行时,你可以像这样传递一个lambda函数:

var t = Task.Run(async () =>
        {
            await Task.Delay(TimeSpan.FromSeconds(1.5));
            return 42;
        });