【问题标题】:async Task Main() in WinForms application having async initialization具有异步初始化的 WinForms 应用程序中的异步任务 Main()
【发布时间】:2018-09-28 20:51:17
【问题描述】:

我们有一个使用异步初始化过程的 winforms 应用程序。简而言之,您可以说应用程序将运行以下步骤:

  • 初始化 - 异步运行
  • 显示主窗体
  • Application.Run()

当前存在且工作的代码如下所示:

[STAThread]
private static void Main()
{
    SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());

    var task = StartUp();
    HandleException(task);

    Application.Run();
}

private static async Task StartUp()
{
    await InitAsync();

    var frm = new Form();
    frm.Closed += (_, __) => Application.ExitThread();
    frm.Show();
}

private static async Task InitAsync()
{
    // the real content doesn't matter
    await Task.Delay(1000);
}

private static async void HandleException(Task task)
{
    try
    {
        await Task.Yield();
        await task;
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        Application.ExitThread();
    }
}

Mark Sowul here 非常详细地描述了它的工作原理。

从 C# 7.1 开始,我们可以在 main 方法中使用异步任务。我们以直接的方式进行了尝试:

[STAThread]
private static async Task Main()
{
    SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());

    try
    {
        await StartUp();
        Application.Run();
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        Application.ExitThread();
    }
}

private static async Task StartUp()
{
    await InitAsync();

    var frm = new Form();
    frm.Closed += (_, __) => Application.ExitThread();
    frm.Show();
}

private static async Task InitAsync()
{
    // the real content doesn't matter
    await Task.Delay(1000);
}

但这不起作用。原因很清楚。第一个await 之后的所有代码都将被转发到消息循环。但是消息循环还没有开始,因为启动它的代码 (Application.Run()) 位于第一个 await 之后。

删除同步上下文将解决问题,但会导致在不同线程中运行await 之后的代码。

将代码重新排序以在第一个 await 之前调用 Application.Run() 将不起作用,因为这是一个阻塞调用。

我们尝试使用具有async Task Main() 的新功能,它允许我们删除难以理解的HandleException-解决方案。但我们不知道怎么做。

你有什么建议吗?

【问题讨论】:

    标签: winforms async-await main c#-7.1


    【解决方案1】:

    您不需要async Main。以下是可能的方法:

    [STAThread]
    static void Main()
    {
        void threadExceptionHandler(object s, System.Threading.ThreadExceptionEventArgs e)
        {
            Console.WriteLine(e);
            Application.ExitThread();
        }
    
        async void startupHandler(object s, EventArgs e)
        {
            // WindowsFormsSynchronizationContext is already set here
            Application.Idle -= startupHandler;
    
            try
            {
                await StartUp();
            }
            catch (Exception)
            {
                // handle if desired, otherwise threadExceptionHandler will handle it
                throw;
            }
        };
    
        Application.ThreadException += threadExceptionHandler;
        Application.Idle += startupHandler;
        try
        {
            Application.Run();
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
        finally
        {
            Application.Idle -= startupHandler;
            Application.ThreadException -= threadExceptionHandler;
        }
    }
    

    注意,如果您不注册threadExceptionHandler 事件处理程序和StartUp throws(或者消息循环抛出的任何其他内容),它仍然可以工作。异常将在包裹Application.Runtry/catch 中捕获。它只是一个TargetInvocationException 异常,原始异常可通过其InnerException 属性获得。

    更新以解决 cmets:

    但是对我来说,注册一个 EventHandler 到 空闲事件,因此启动整个应用程序。完全清楚如何 这有效,但仍然很奇怪。在这种情况下,我更喜欢 我已经拥有的 HandleException 解决方案。

    我想这是一个品味问题。我不知道为什么 WinForms API 设计者没有提供像 WPF 的 Application.Startup 这样的东西。但是,由于在 WinForm 的 Application 类上没有专门的事件,在第一个 Idle 事件上延迟特定的初始化代码是 IMO 一个优雅的解决方案,它在 SO 上被广泛使用。

    我特别不喜欢 WindowsFormsSynchronizationContext 的显式手动配置 之前 Application.Run 已经开始,但如果你想要一个替代解决方案,你去:

    [STAThread]
    static void Main()
    {
        async void startupHandler(object s)
        {
            try
            {
                await StartUp();
            }
            catch (Exception ex)
            {
                // handle here if desired, 
                // otherwise it be asynchronously propogated to 
                // the try/catch wrapping Application.Run 
                throw;
            }
        };
    
        // don't dispatch exceptions to Application.ThreadException 
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
    
        using (var ctx = new WindowsFormsSynchronizationContext())
        {
            System.Threading.SynchronizationContext.SetSynchronizationContext(ctx);
            try
            {
                ctx.Post(startupHandler, null);
                Application.Run();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
            finally
            {
                System.Threading.SynchronizationContext.SetSynchronizationContext(null);
            }
        }
    }
    

    IMO,任何一种方法都比您问题中使用的方法更干净。附带说明一下,您应该使用ApplicationContext 来处理表单关闭。您可以将ApplicationContext 的实例传递给Application.Run

    我是唯一的一点 缺少的是您已设置同步上下文的提示。 是的,但为什么呢?

    它确实被设置为Application.Run 的一部分,如果当前线程上还没有的话。如果您想了解更多详细信息,可以在.NET Reference Source 进行调查。

    【讨论】:

    • 感谢您的回答。我测试了它 - 当然 - 它有效。但对我来说,将 EventHandler 注册到空闲事件以便启动整个应用程序看起来很奇怪。完全清楚这是如何工作的,但仍然很奇怪。在这种情况下,我更喜欢我已经拥有的HandleException 解决方案。我缺少的唯一一点是您暗示已经设置了同步上下文。是的 - 但为什么呢?我知道它是在创建第一个控件时初始化的。也许Application.Run() 也会初始化它。
    • 顺便说一句,我们遇到了类似的问题,但我们根本不想调用 Application.Run() (对于具有一些奇怪约束的混合 CLI/GUI 应用程序......)。感谢您的回答和stackoverflow.com/a/57596559/755986,我们通过将我们的异步代码包装在 AsyncContext.Run(...)(AsyncContext by Stephen Cleary)中解决了这个问题
    猜你喜欢
    • 1970-01-01
    • 2017-11-02
    • 1970-01-01
    • 1970-01-01
    • 2016-04-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多