我正在尝试异步显示一个进度表,其中说应用程序正在运行,而实际应用程序正在运行。
如下this question所示,我有以下内容:
主表单:
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
async Task<int> LoadDataAsync()
{
await Task.Delay(2000);
return 42;
}
private async void Run_Click(object sender, EventArgs e)
{
var runningForm = new RunningForm();
runningForm.ShowRunning();
var progressFormTask = runningForm.ShowDialogAsync();
var data = await LoadDataAsync();
runningForm.Close();
await progressFormTask;
MessageBox.Show(data.ToString());
}
}
进度表
public partial class RunningForm : Form
{
private readonly SynchronizationContext synchronizationContext;
public RunningForm()
{
InitializeComponent();
synchronizationContext = SynchronizationContext.Current;
}
public async void ShowRunning()
{
this.RunningLabel.Text = "Running";
int dots = 0;
await Task.Run(() =>
{
while (true)
{
UpadateUi($"Running{new string('.', dots)}");
Thread.Sleep(300);
dots = (dots == 3) ? 0 : dots + 1;
}
});
}
public void UpadateUi(string text)
{
synchronizationContext.Post(
new SendOrPostCallback(o =>
{
this.RunningLabel.Text = text;
}),
text);
}
public void CloseThread()
{
synchronizationContext.Post(
new SendOrPostCallback(o =>
{
this.Close();
}),
null);
}
}
internal static class DialogExt
{
public static async Task<DialogResult> ShowDialogAsync(this Form form)
{
await Task.Yield();
if (form.IsDisposed)
{
return DialogResult.OK;
}
return form.ShowDialog();
}
}
上面的方法工作正常,但是当我从另一个外部进行呼叫时却无法正常工作。这是我的控制台应用程序:
class Program
{
static void Main(string[] args)
{
new Test().Run();
Console.ReadLine();
}
}
class Test
{
private RunningForm runningForm;
public async void Run()
{
var runningForm = new RunningForm();
runningForm.ShowRunning();
var progressFormTask = runningForm.ShowDialogAsync();
var data = await LoadDataAsync();
runningForm.CloseThread();
await progressFormTask;
MessageBox.Show(data.ToString());
}
async Task<int> LoadDataAsync()
{
await Task.Delay(2000);
return 42;
}
}
看着调试器发生了什么,该过程进入await Task.Yield()
,并且从未进行到return form.ShowDialog()
,因此您从未见过RunningForm
。然后,该过程转到LoadDataAsync()
,并永久挂在await Task.Delay(2000)
上。
为什么会这样?与Task
的优先级(即:Task.Yield()
)的优先级有关吗?
答案 0 :(得分:2)
观看调试器发生的情况,等待该过程 Task.Yield()永远不会返回form.ShowDialog(),因此 您永远不会看到RunningForm。然后该过程转到 LoadDataAsync()并永久挂起,等待Task.Delay(2000)。
为什么会这样?
这里发生的是,当您在没有任何同步上下文的控制台线程上执行var runningForm = new RunningForm()
时(System.Threading.SynchronizationContext.Current
为null),它将隐式创建WindowsFormsSynchronizationContext
的实例并将其安装在当前线程,有关此here的更多信息。
然后,当您按下await Task.Yield()
时,ShowDialogAsync
方法将返回给调用者,并且await
延续将发布到该新的同步上下文中。但是,继续操作永远不会有被调用的机会,因为当前线程不会运行消息循环,发布的消息也不会被泵送。没有死锁,但是await Task.Yield()
之后的代码从未执行,因此对话框甚至不会显示。 await Task.Delay(2000)
也是如此。
我对了解为什么它适用于WinForms而不适用于WinForms更感兴趣 控制台应用程序。
您需要在控制台应用程序中使用带有消息循环的UI线程。尝试像这样重构您的控制台应用程序:
public void Run()
{
var runningForm = new RunningForm();
runningForm.Loaded += async delegate
{
runningForm.ShowRunning();
var progressFormTask = runningForm.ShowDialogAsync();
var data = await LoadDataAsync();
runningForm.Close();
await progressFormTask;
MessageBox.Show(data.ToString());
};
System.Windows.Forms.Application.Run(runningForm);
}
这里,Application.Run
的工作是启动模式消息循环(并在当前线程上安装WindowsFormsSynchronizationContext
),然后显示表单。 runningForm.Loaded
异步事件处理程序是在该同步上下文上调用的,因此它内部的逻辑应按预期工作。
但是,这使Test.Run
是一种同步方法,即例如,仅在关闭表单且消息循环结束时才返回。如果这不是您想要的,则必须创建一个单独的线程来运行消息循环,就像我做的with MessageLoopApartment
here。
也就是说,在典型的WinForms或WPF应用程序中,您几乎永远不需要辅助UI线程。