异步初始化

时间:2018-04-18 14:42:07

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

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

  • Init - 运行async
  • 显示MainForm
  • 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方法中使用async Task。我们直接尝试了它:

[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之后的其他线程中运行代码。

重新排序代码以便在第一个Application.Run()之前调用await将无效,因为它是阻止调用。

我们尝试使用async Task Main()的新功能,允许我们删除难以理解的HandleException解决方案。但我们不知道如何。

你有什么建议吗?

1 个答案:

答案 0 :(得分: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抛出(或者消息循环中的任何其他内容抛出,那么)它仍然可以工作。异常将被包含在try/catch的{​​{1}}内部捕获。它只是一个Application.Run异常,原始异常通过其TargetInvocationException属性提供。

更新了以解决评论:

  

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

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

我特别不喜欢在 Idle开始之前明确手动配置WindowsFormsSynchronizationContext ,但如果您想要一个替代解决方案,请转到:

Application.Run

IMO,这两种方法都比你问题中使用的方法更干净。另外,您应该使用ApplicationContext来处理表单关闭。您可以将[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); } } } 的实例传递给ApplicationContext

  

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

它确实设置为Application.Run的一部分,如果当前线程上尚未存在的话。如果您想了解更多详情,可以在.NET Reference Source进行调查。