我对使用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中异步运行代码?


当前回答

最新版本的c# - c# 7.1允许创建异步控制台应用程序。要在项目中启用c# 7.1,你必须将VS升级到至少15.3,并将c#版本更改为c# 7.1或c#最新的次要版本。要做到这一点,请转到项目属性->构建->高级->语言版本。

在此之后,以下代码将工作:

internal class Program
{
    public static async Task Main(string[] args)
    {
         (...)
    }

其他回答

当c# 5 CTP被引入时,你当然可以用async…尽管这样做通常不是个好主意。我相信这是在VS 2013的发布中改变的,成为一个错误。

除非你已经启动了任何其他前台线程,否则你的程序将在Main完成时退出,即使它已经启动了一些后台工作。

你到底想做什么?请注意,你的GetList()方法目前真的不需要是异步的——它添加了一个额外的层,没有真正的原因。它在逻辑上等价于(但比):

public Task<List<TvChannel>> GetList()
{
    return new GetPrograms().DownloadTvChannels();
}

正如你所发现的,在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包装。

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>    

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

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

(转换仅用于解决歧义)

下面的代码可以用来创建一个主异步。我已经调整它使用长时间运行的任务(了解更多信息在这里:https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-7.0)

它还实现了来自上述响应的取消令牌。

    private static int Main(string[] args)
    {
        var cts = new CancellationTokenSource();
        Console.CancelKeyPress += (s, e) =>
        {
            e.Cancel = !cts.IsCancellationRequested;
            cts.Cancel();
            Console.WriteLine("CancellationRequested");
        };

        try
        {
            var task = new Task<int>(
                () => MainAsync(args, cts.Token).GetAwaiter().GetResult(), 
                cts.Token,
                TaskCreationOptions.LongRunning //https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-7.0
            );
            task.Start();
            var exitCode =  task.GetAwaiter().GetResult();      
            /*Or this.*/
            //var exitCode = MainAsync(args, cts.Token).GetAwaiter().GetResult();
            return exitCode;// MainAsync(args, cts.Token).GetAwaiter().GetResult();
        } 
        catch (OperationCanceledException ex)
        {
            Console.WriteLine(ex);
            return 1223; // Cancelled.
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex);
            return -1;
        }
    }
    private static async Task<int> MainAsync(string[] args, CancellationToken cancellationToken)
    {
         await Something()
         return;
    }

在下面的例子中,我写了。你可以使用maxDegreeOfParallelism & numberOfIteration来理解/查看任务是如何处理的。学习TPL很好的起点!

private static async Task<int> MainAsync(string[] args, CancellationToken cancellationToken)
    {
        
        var infos = new ConcurrentBag<Info>();
        var mySuperUselessService = new BigWorkload();

        int numberOfSecond = 1;
        int numberOfIteration = 25;     //Experiment with this
        int maxDegreeOfParallelism = 4; //Experiment with this

        var simulateWorkTime = TimeSpan.FromSeconds(numberOfSecond);
        var informations = Enumerable.Range(1, numberOfIteration)
            .Select(x => new Info() { Index = x });

        var count = informations.Count();
        var chunkNeeded = Math.Round(count / Convert.ToDecimal(maxDegreeOfParallelism), MidpointRounding.ToPositiveInfinity);

        var splashInfo = @$"
Press CTRL + C to cancel. 
Processing {count} items, maxDegreeOfParallelism set to {maxDegreeOfParallelism}.
But it will be bound by the core on the machine {Environment.ProcessorCount}. 
This operation should take ~{chunkNeeded * (numberOfSecond + 0.01m)}s
And will be starting test in 2s
";
        Console.WriteLine(splashInfo);
        await Task.Delay(TimeSpan.FromSeconds(2));

        var parralelOptions = new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism, CancellationToken = cancellationToken};
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        var forLoopTask = Parallel.ForEachAsync(informations, parralelOptions, async (info, token) =>
        {
            await mySuperUselessService.Simulate(simulateWorkTime, info);
            Console.WriteLine(info);
            infos.Add(info);


        });
        await forLoopTask;
        stopwatch.Stop();

        foreach (var grouped in infos.GroupBy(x => x.ManagedThreadId))
        {
            Console.WriteLine($"ThreadId: {grouped.Key}");
            foreach (var item in grouped)
            {
                Console.WriteLine($"\t Index: {item.Index} {item.TaskCurrentId}");
                
            }
        }
        Console.WriteLine($"NumberOfThread: {infos.GroupBy(x => x.ManagedThreadId).Count()}");
        Console.WriteLine($"Elasped: {stopwatch.ElapsedMilliseconds / 1000d}s");
        Console.WriteLine(splashInfo);

        return 0;
        
    }