我有一个使用异步方法连接到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()
作为同步方法来运行它,但这在第一次调用时就死锁了。
如何处理并让它运行异步发行版,等待它完成,然后关闭?
答案 0 :(得分:2)
您要执行的操作中的基本问题是async / await假定主应用程序线程继续运行。该假设与关闭操作直接冲突,后者的工作是终止所有正在运行的任务。
如果您查看Window_Closing
上的文档,则说明以下内容(仅以下内容):
在调用Close()之后直接发生,可以进行处理以取消窗口关闭。
这很重要。唯一应该做的就是允许您以编程方式取消窗口关闭,从而提示一些其他用户操作。
由于异步/等待的工作方式,您的期望被迷惑了。异步/等待出现以线性方式运行;但是,实际发生的情况是控制在第一个await
处传递回了调用方。此时,框架假定您不希望取消关闭表单,并允许程序终止并执行所有其他操作。
基本上,所有C风格的程序都有一个主入口点,该入口点运行一个循环。从C的早期开始就是这种方式,并且在WPF中一直如此。但是,在WPF中,Microsoft有点聪明,因此决定将其隐藏在程序员面前。有两个选项可以处理在主窗口关闭后 发生的事情:
重新劫持程序中的主循环,并将代码放在此处。有关如何执行此操作的详细信息,请参见here。
设置一个explicit shutdown mode,然后开始执行该任务。调用Application.Shutdown()
作为您需要执行的最后一行代码。
答案 1 :(得分:1)
这是FormClosing
事件的异步版本。它会延迟关闭表单,直到提供的Task
完成为止。阻止用户在任务完成之前关闭表单。
OnFormClosingAsync
事件将FormClosingEventArgs
类的增强版本传递给处理代码,并带有两个附加属性:bool HideForm
和int 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设置为在最后一个窗口或主窗口关闭时关闭(我相信主窗口关闭是默认的),那么您将遇到以下注释中所述的竞争状态,并且可能在关闭之前关闭线程事情完成了。