.NET异步关闭方法?

时间:2019-10-16 05:36:32

标签: c# .net wpf asynchronous

我有一个使用异步方法连接到REST API的应用程序。我几乎在连接到API的所有地方都使用了异步/等待设置,但是我有一个问题和一些我不完全了解的奇怪行为。我要做的只是在程序关闭时在某些情况下返回许可证。这由关闭窗口事件启动;事件处理程序如下:

async void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            ...other synchronous code...

            //Check for floating licensing
            if (KMApplication.License != null && KMApplication.License.Scope != Enums.LicenseScope.Standalone)
            {
                for (int i = 0; i < 3; i++)
                {
                    try
                    {

                        await KMApplication.License.ShutDown(KMApplication.Settings == null
                                                                 ? Enums.LicenseReturnModes.PromptOnShutdown
                                                                 : KMApplication.Settings.LicenseReturnMode)
                                           .ConfigureAwait(false);
                        break;
                    }
                    catch (Exception ex)
                    {
                        _logger.Warn("Exception in license release, attempt " + i, ex);
                    }
                }
            }

            await KMApplication.ApiService.Disconnect().ConfigureAwait(false);

            _logger.Info("Shutdown Complete");

            Application.Current?.Shutdown();
        }

运行此命令后,我可以逐步进入调试器,并转到第一个许可证关闭调用,这是第一个异步等待的调用。然后,当我按F10键进入下一行代码时,它只是关闭并消失了。我验证了应该在该行中进行的许可证发布正在进行中,因此它似乎可以运行到该行的完成,但是随后关闭或崩溃或发生其他情况。我还查看了日志,但它从未到达Shutdown Complete行,而且我也不认为它也到达了ApiService.Disconnect

我也尝试使用Task.Run(() => ...the method...).GetAwaiter().GetResult()作为同步方法来运行它,但这在第一次调用时就死锁了。

如何处理并让它运行异步发行版,等待它完成,然后关闭?

3 个答案:

答案 0 :(得分:2)

您要执行的操作中的基本问题是async / await假定主应用程序线程继续运行。该假设与关闭操作直接冲突,后者的工作是终止所有正在运行的任务。

如果您查看Window_Closing上的文档,则说明以下内容(仅以下内容):

  

在调用Close()之后直接发生,可以进行处理以取消窗口关闭。

这很重要。唯一应该做的就是允许您以编程方式取消窗口关闭,从而提示一些其他用户操作。

由于异步/等待的工作方式,您的期望被迷惑了。异步/等待出现以线性方式运行;但是,实际发生的情况是控制在第一个await处传递回了调用方。此时,框架假定您不希望取消关闭表单,并允许程序终止并执行所有其他操作。

基本上,所有C风格的程序都有一个主入口点,该入口点运行一个循环。从C的早期开始就是这种方式,并且在WPF中一直如此。但是,在WPF中,Microsoft有点聪明,因此决定将其隐藏在程序员面前。有两个选项可以处理在主窗口关闭后 发生的事情:

  1. 重新劫持程序中的主循环,并将代码放在此处。有关如何执行此操作的详细信息,请参见here

  2. 设置一个explicit shutdown mode,然后开始执行该任务。调用Application.Shutdown()作为您需要执行的最后一行代码。

答案 1 :(得分:1)

这是FormClosing事件的异步版本。它会延迟关闭表单,直到提供的Task完成为止。阻止用户在任务完成之前关闭表单。

OnFormClosingAsync事件将FormClosingEventArgs类的增强版本传递给处理代码,并带有两个附加属性:bool HideFormint Timeout。这些属性是读/写的,非常类似于现有的Cancel属性。将HideForm设置为true的作用是在进行异步操作时隐藏表单,以避免使用户感到沮丧。将Timeout设置为大于0的值的作用是在以毫秒为单位的指定持续时间后放弃异步操作,并关闭表单。否则,有可能使用隐藏的UI使应用程序无限期地运行,这肯定是一个问题。 Cancel属性仍然可用,并且可以由事件处理程序设置为true,以防止表单关闭。

static class WindowsFormsAsyncExtensions
{
    public static IDisposable OnFormClosingAsync(this Form form,
        Func<object, FormClosingAsyncEventArgs, Task> handler)
    {
        Task compositeTask = null;
        form.FormClosing += OnFormClosing; // Subscribe to the event
        return new Disposer(() => form.FormClosing -= OnFormClosing);

        async void OnFormClosing(object sender, FormClosingEventArgs e)
        {
            if (compositeTask != null)
            {
                // Prevent the form from closing before the task is completed
                if (!compositeTask.IsCompleted) { e.Cancel = true; return; }
                // In case of success allow the form to close
                if (compositeTask.Status == TaskStatus.RanToCompletion) return;
                // Otherwise retry calling the handler
            }
            e.Cancel = true; // Cancel the normal closing of the form
            var asyncArgs = new FormClosingAsyncEventArgs(e.CloseReason);
            var handlerTask = await Task.Factory.StartNew(
                () => handler(sender, asyncArgs),
                CancellationToken.None, TaskCreationOptions.DenyChildAttach,
                TaskScheduler.Default); // Start in a thread-pool thread
            var hideForm = asyncArgs.HideForm;
            var timeout = asyncArgs.Timeout;
            if (hideForm) form.Visible = false;
            compositeTask = Task.WhenAny(handlerTask, Task.Delay(timeout)).Unwrap();
            try
            {
                await compositeTask; // Await and then continue in the UI thread
            }
            catch (OperationCanceledException) // Treat this as Cancel = true
            {
                if (hideForm) form.Visible = true;
                return;
            }
            catch // On error don't leave the form hidden
            {
                if (hideForm) form.Visible = true;
                throw;
            }
            if (asyncArgs.Cancel) // The caller requested to cancel the form close
            {
                compositeTask = null; // Forget the completed task
                if (hideForm) form.Visible = true;
                return;
            }
            await Task.Yield(); // Ensure that form.Close will run asynchronously
            form.Close(); // Finally close the form
        }
    }

    private struct Disposer : IDisposable
    {
        private readonly Action _action;
        public Disposer(Action disposeAction) => _action = disposeAction;
        void IDisposable.Dispose() => _action?.Invoke();
    }
}

public class FormClosingAsyncEventArgs : EventArgs
{
    public CloseReason CloseReason { get; }
    private volatile bool _cancel;
    public bool Cancel { get => _cancel; set => _cancel = value; }
    private volatile bool _hideForm;
    public bool HideForm { get => _hideForm; set => _hideForm = value; }
    private volatile int _timeout;
    public int Timeout { get => _timeout; set => _timeout = value; }

    public FormClosingAsyncEventArgs(CloseReason closeReason) : base()
    {
        this.CloseReason = closeReason;
        this.Timeout = System.Threading.Timeout.Infinite;
    }
}

由于OnFormClosingAsync是扩展方法,而不是实际事件,因此它只能有一个处理程序。

用法示例:

public Form1()
{
    InitializeComponent();
    this.OnFormClosingAsync(Window_FormClosingAsync);
}

async Task Window_FormClosingAsync(object sender, FormClosingAsyncEventArgs e)
{
    e.HideForm = true; // Optional
    e.Timeout = 5000; // Optional
    await KMApplication.License.ShutDown();
    //e.Cancel = true; // Optional
}

Window_FormClosingAsync处理程序将在线程池线程中运行,因此它不应包含任何UI操作代码。

可以通过保留IDisposable返回值的引用并进行处理来取消订阅该事件。


更新:阅读this answer之后,我意识到可以在表单中添加真实事件FormClosingAsync,而无需创建从表单继承的类。这可以通过添加事件,然后运行将事件挂接到本机FormClosing事件的初始化方法来实现。像这样:

public event Func<object, FormClosingAsyncEventArgs, Task> FormClosingAsync;

public Form1()
{
    InitializeComponent();
    this.InitFormClosingAsync(() => FormClosingAsync);

    this.FormClosingAsync += Window_FormClosingAsync_A;
    this.FormClosingAsync += Window_FormClosingAsync_B;
}

在初始化程序内部,在本机FormClosing事件的内部处理程序中,可以检索该事件的所有订阅者 使用GetInvocationList方法:

var eventDelegate = handlerGetter();
if (eventDelegate == null) return;
var invocationList = eventDelegate.GetInvocationList()
    .Cast<Func<object, FormClosingAsyncEventArgs, Task>>().ToArray();

...然后适当地调用。所有这些都增加了复杂性,同时还讨论了允许多个处理程序的实用性。因此,我可能会坚持使用原始的单处理程序设计。


更新:仍然可以使用原始方法OnFormClosingAsync使用多个处理程序。实际上,这很容易。 Func<T> 类继承自Delegate,因此它具有像真实事件一样的调用列表:

Func<object, FormClosingAsyncEventArgs, Task> aggregator = null;
aggregator += Window_FormClosingAsync_A;
aggregator += Window_FormClosingAsync_B;
this.OnFormClosingAsync(aggregator);

不需要在OnFormClosingAsync方法中进行修改。

答案 2 :(得分:0)

好,这就是我最后要做的。基本上,关闭窗口会启动一个任务,该任务将等待释放发生,然后调用关闭命令。这是我以前试图做的,但是它似乎不能在异步void方法中工作,但似乎是在以这种方式完成的。这是新的处理程序:

void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        ...other sync code...

        Task.Run(async () =>
        {
            await InvokeKmShutdown();
            (Dispatcher ?? Dispatcher.CurrentDispatcher).InvokeShutdown();
        });
    }

关闭方法如下:

async Task InvokeKmShutdown()
    {
        ...other sync code...

        await KMApplication.ApiService.Disconnect();

        //Check for floating licensing
        if (KMApplication.License != null && KMApplication.License.Scope != License.Core.Enums.LicenseScope.Standalone)
        {
            for (int i = 0; i < 3; i++)
            {
                try
                {

                    await KMApplication.License.ShutDown(KMApplication.Settings == null
                                                             ? Enums.LicenseReturnModes.PromptOnShutdown
                                                             : KMApplication.Settings.LicenseReturnMode);
                    break;
                }
                catch (Exception ex)
                {
                    _logger.Warn("Exception in license release, attempt " + i, ex);
                }
            }
        }
    }

希望它可以帮助某人。

编辑

请注意,这是在App.xaml中将WPF应用程序设置为ShutdownMode="OnExplicitShutdown"的情况,因此在我调用关闭程序之前,它不会关闭实际的应用程序。如果您使用WinForms或WPF设置为在最后一个窗口或主窗口关闭时关闭(我相信主窗口关闭是默认的),那么您将遇到以下注释中所述的竞争状态,并且可能在关闭之前关闭线程事情完成了。