C#异步方法调用一直到主函数

5

有人可以解释一下这个例子吗?当然,这个例子目前不起作用:

class Program
{
    static void Main(string[] args)//main cant' be async
    {
        int res = test();//I must put await here

        Console.WriteLine("END");
    }

    public async static Task<int> test()
    { //why can't I make it just: public int test()??
        int a1, a2, a3, a4;

        a1 = await GetNumber1();
        a2 = await GetNumber2();
        a3 = await GetNumber3();

        a4 = a1 + a2 + a3;
        return a4;
    }

    public static async Task<int> GetNumber1()
    {
        await Task.Run(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("GetNumber1");
                    System.Threading.Thread.Sleep(100);
                }
            });
        return 1;
    }

我正在尝试使用"await"从GenNumberX方法中“收集”值。我想以某种方式使方法“test”不是异步的。当我使用await获取值时,我不明白为什么测试必须是异步的。这个例子让我在每个使用异步的方法上编写async,当我向上钻取到Main时,我无法使它成为异步的?
如何编写实际示例:
public bool ReserveAHolliday()
{
        bool hotelOK = await ReserveAHotel();//HTTP async request
        bool flightOK = await ReserveFlight();////HTTP async request
        bool result = hotelOK && flightOK;
        return result;
}

如何将方法ReserveAHolliday同步化? 我是不是遗漏了什么或者没有理解异步等待机制的使用方式?


1
在某些时候,您需要从同步方法启动异步任务。如果您想要触发它,只需调用它(也称为异步voids、fire and forget),或使用test().Wait()来同步等待任务。 - Blue
6个回答

2

以下是一个完整的示例。您可以从MAIN同步运行ReserverAHoliday(bool r = ReserveAHolliday().Result;)和异步运行(只需调用ReserveAHolliday();)(取决于您注释哪一行)。您可以看到效果(“END”在预订完成之前/之后打印)。 我更喜欢使用await Task.WhenAll()方法,它更易读。 还请注意,在GetNumber1中使用await Task.Delay(100)而不是Thread.sleep更好。

    class Program
{
    static void Main(string[] args)//main cant' be async
    {
        //int res = test().Result;//I must put await here
        bool r = ReserveAHolliday().Result; //this will run Synchronously.
        //ReserveAHolliday(); //this option will run aync : you will see "END" printed before the reservation is complete.
        Console.WriteLine("END");
        Console.ReadLine();
    }

    public async static Task<int> test()
    { //why can't I make it just: public int test()??
        //becuase you cannot use await in synchronous methods. 
        int a1, a2, a3, a4;

        a1 = await GetNumber1();
        a2 = await GetNumber1();
        a3 = await GetNumber1();

        a4 = a1 + a2 + a3;
        return a4;
    }

    public static async Task<int> GetNumber1()
    {
        //await Task.Run(() =>
        //    {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("GetNumber1");
                    await Task.Delay(100); // from what I read using Task.Delay is preferred to using  System.Threading.Thread.Sleep(100);
                }
        //    });
        return 1;
    }

    public async static Task<bool> ReserveAHolliday()
    {
        //bool hotelOK = await ReserveAHotel();//HTTP async request
        //bool flightOK = await ReserveAHotel();////HTTP async request
        var t1 = ReserveAHotel("FirstHotel");
        var t2 = ReserveAHotel("SecondHotel");
        await Task.WhenAll(t1, t2);
        bool result = t1.Result && t1.Result;// hotelOK && flightOK;
        return result;
    }
    public static async Task<bool> ReserveAHotel(string name)
    {
        Console.WriteLine("Reserve A Hotel started for "+ name);
        await Task.Delay(3000);
        if (name == "FirstHotel") 
            await Task.Delay(500); //delaying first hotel on purpose.
        Console.WriteLine("Reserve A Hotel done for " + name);
        return true;
    }
}

1
谢谢,这个例子会像@Kabbalah在上面的回答中提到的那样造成死锁吗? 我应该保持这些方法异步,因为我将编写一个库供WinForms项目使用。这段代码只是一个示例。实际上,我正在编写SignalR客户端/包装器,以便使用_IsUserOnline(string userId)_,_GetOnlineUsers_等方法在各种软件之间进行通信(ERP-> POS,POS-> ERP)。 - vpetrovic
在WinForms中,这可能取决于你的库发生了什么。你的库代码是否需要同步上下文?如果是,那么它肯定会死锁。如果不是,只要在库代码中指定ConfigureAwait(false),你可以摆脱这个问题。我会编辑我的答案,提供更多信息。 - Kabbalah
我建议花时间研究一下,以免受到伤害。 如果您使用await,您会更安全,因为您处于异步上下文中,并且最初调用异步方法的同步代码可以继续工作... 为了更加确定其影响,我建议在您的异步方法中添加长时间延迟,这样您就可以亲自看到其行为影响。 此外,我更喜欢从一个布尔属性(propfull)启动这样的异步,我将其用作标志(这样当我不想要时,同一任务不会被多次并行启动)。我不知道这是否适用于您,以及它是否是良好的实践。 - Guy

1

首先,永远不要使用 Result,它的实现不好,你最终会遇到死锁问题。相反,请使用GetAwaiter().GetResult()

如果您使用的是C# 7.0或更低版本,则可以安全地使用此模式:

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

    private static async Task MainAsync(string[] args)
    {
        await MyMethod();
        await MyOtherMethod();
        Console.WriteLine("Done!");
    }

    private static async Task MyMethod()
    {
        // do something
        await Task.CompletedTask;
    }

    private static async Task MyOtherMethod()
    {
        // do something
        await Task.CompletedTask;
    }

如果您正在使用C#7.1+,则可以像这样将主方法更改为异步方法:
    static async Task Main(string[] args)
    {
        await MyMethod();
        await MyOtherMethod();
        Console.WriteLine("Done!");
    }

    private static async Task MyMethod()
    {
        // do something
        await Task.CompletedTask;
    }

    private static async Task MyOtherMethod()
    {
        // do something
        await Task.CompletedTask;
    }

在 .Net Core 项目中,您可以使用以下标签将版本更改为 C# 7.1 或更高版本:
<LangVersion>7.1</LangVersion>

或者

<LangVersion>latest</LangVersion>

项目文件将会长成这个样子:

<Project Sdk="Microsoft.NET.Sdk">    
 <PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>netcoreapp2.1</TargetFramework>
  <LangVersion>latest</LangVersion>
 </PropertyGroup>   
</Project>

0
为了在方法内使用await关键字,它必须是async。这是一种设计决策,使其更容易向后兼容。
由于Main是程序的起点,因此不能是async,因此通常需要以某种方式阻止程序运行。您可以通过阻塞Task甚至调用Console.ReadLine来实现。
任务通常在后台线程上执行,而后台线程如果所有前台线程停止,则不会保持程序运行。
我在这里有一篇介绍性的博客文章,讲解了async-awaithere

0

如果您在ReserveAHolliday方法内使用await关键字,则无法将其设置为同步。

可能的解决方法是简单地阻塞,直到两个异步方法都返回其值。但我不建议这样做,因为它可能会导致死锁或其他意外错误。

如果您没有同步上下文(例如控制台应用程序),则可以简单地编写:

bool hotelOK = ReserveAHotel().Result;

但我想你最终想要创建一个GUI。这就是阻塞失效的地方。因为UI应用程序(Forms和WPF)确实有上下文,等待异步方法会导致死锁。在此处阅读更多信息

对于这个问题有解决方法,但它们高度依赖于您的应用程序设计。

编辑:

如果您的库代码需要同步上下文,则阻塞肯定会导致死锁。

如果您不需要它,则可以在库代码中使用ConfigureAwait(false)并避免阻塞。

我的建议是:始终使用async/await或完全不使用async/await。还有其他使您的代码异步化的可能性,例如:基于事件的,但像生活中的一切一样,都有优缺点。

请阅读Stephen Cleary的这篇博客。他解释了为什么阻塞异步代码会导致死锁以及如何避免它。


是的,我正在编写一个库项目,将在WinForms GUI中使用。如果这可能会造成一些问题,我将保持这些方法_async_,这不是什么大问题,我只是想按照我计划的方式设计这些类,但这个计划可能是错误的。 - vpetrovic

0

你的异步操作需要从某个地方开始,而由于你的程序有一个起点,所以在这一点上它是同步方法。大多数 async 操作都从我们称之为 async void 的方法开始,这些方法基本上是“启动并忘记”方法。它启动了一个任务,并不关心它返回什么。如果你需要在同步方法中等待某些东西,你可以在任务上使用 .Wait() 方法,或者使用 .Result 来获取任务的结果。

对于你的真实世界示例,如果你想同时运行两个任务,你需要做类似以下的事情:

public async Task<bool> ReserveAHoliday()
{
        //Initialize and start this task
        Task<bool> hotelTask = ReserveAHotel();//HTTP async request
        //Initialize and start this task
        Task<bool> flightTask = ReserveFlight();////HTTP async request
        //Await until hotel task is done, and get the bool
        bool hotelOK = await hotelTask;
        //Await until flight task is done (This might have already finished while the hotel was grabbing, and get the bool
        bool flightOK = await flightTask;
        bool result = hotelOK && flightOK;
        //Return the final result
        return result;
}

我强烈推荐观看这个视频。它很好地介绍了异步工作的原理,可以让你快速进入异步编程的美妙世界。


ReserveAHolliday 无法工作,因为使用 await 而未声明方法为异步。所以似乎我必须使用 Task.Wait() 方法。 - vpetrovic
我已经更新了示例。ReserveAHoliday 应该是一个异步任务。正如我所提到的,我强烈建议您查看我链接的视频。这可能需要一些时间,但非常值得。 - Blue

0

感谢FrankerZ提供正确的答案! 因此,我的小例子应该看起来像这样:

class Program
{
    static void Main(string[] args)
    {
        var task = test();
        task.Wait(); //this stops async behaviour
        int result = task.Result;// get return value form method "test"
        Console.WriteLine("RESULT IS = " + result);
    }

    public async static Task<int> test()
    {
        int a1, a2, a3, a4;

        //run all tasks
        //all tasks are doing their job "at the same time"
        var taskA1 = GetNumber1();
        var taskA2 = GetNumber2();
        var taskA3 = GetNumber3();

        //wait for task results
        //here I am collecting results from all tasks
        a1 = await taskA1;
        a2 = await taskA2;
        a3 = await taskA3;

        //get value from all task results
        a4 = a1 + a2 + a3;
        return a4;
    }

    public static async Task<int> GetNumber1()
    {
        await Task.Run(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("GetNumber1");
                    System.Threading.Thread.Sleep(100);
                }
            });
        return 1;
    }

//其他方法被省略,因为它们与GetNumber1相同 }


1
Task.Result会阻塞直到任务完成,因此您不需要使用task.Wait() - NeddySpaghetti
请注意,这仅适用于控制台应用程序。如果您尝试在WPF或Forms应用程序中使用此方法,很可能会导致死锁。请参阅我发布的链接以了解此问题的详细信息。 - Kabbalah
不要使用Thread.Sleep(100) - 而应该使用await Task.Delay(100) - Daniel Kelley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接