为什么在调用 `Dispatcher.Invoke()` 时 WPF 选项卡变为 "unresponsive"?
Why do WPF tabs become "unresponsive" when `Dispatcher.Invoke()` is called?
据我了解,WPF“消息”(例如按钮单击处理程序)被添加到内部优先级队列中。然后,单个 UI 线程负责处理排队的消息。
遗憾的是,我对 WPF 的了解还不够深入,无法理解该框架的内部工作原理。所以我的问题是,鉴于只有 1 个线程处理消息...
- 导致选项卡变得无响应的内部事件顺序(高级)是什么?
观察到的行为
- 如果缓慢单击,
TabControl
会按预期运行。
- 要重现:每 4 秒单击 1 个选项卡。
- 看来,如果您给
TabControl.SelectedIndex
数据绑定一个完成的机会,控件将按设计运行。
- 如果快速单击选项卡,某些选项卡将变得无响应。
- 重现:在 3 秒内点击尽可能多的标签。
补充阅读
- Two selected tabs in tabcontroller
- 虽然行为相似,但本文有所不同,因为症状是使用
Tab
+ MessageBox
的结果。
示例代码
以下代码可用于重现该行为,从而永久选中 WPF 选项卡。
粘贴到MainWindow.xaml
:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="30" />
<RowDefinition Height="*" />
<RowDefinition Height="30" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<WrapPanel Grid.Row="0">
<TextBlock>
1. Click on as many tabs as possible within 3 seconds.<LineBreak/>
2. Wait until multiple tabs are selected.<LineBreak/>
3. Uncheck the `Simulate Bug` checkbox.<LineBreak/>
</TextBlock>
</WrapPanel>
<CheckBox Grid.Row="1" IsChecked="{Binding CanSimulateBug}" Content="Simulate Bug"/>
<TabControl x:Name="ColorWorkspaces" Grid.Row="2" SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
<TabItem x:Name="RedTab" Header="Red"/>
<TabItem x:Name="OrangeTab" Header="Orange"/>
<TabItem x:Name="YellowTab" Header="Yellow"/>
<TabItem x:Name="GreenTab" Header="Green"/>
<TabItem x:Name="BlueTab" Header="Blue"/>
<TabItem x:Name="VioletTab" Header="Violet"/>
</TabControl>
<TextBlock Grid.Row="3" Text="{Binding Status}"/>
</Grid>
粘贴到 MainWindow.xaml.cs
:
public partial class MainWindow : Window, INotifyPropertyChanged
{
private int _selectedTab;
private string _status;
private bool _canSimulateBug;
public MainWindow()
{
this.CanSimulateBug = true;
this.Status = String.Empty;
this.DataContext = this;
InitializeComponent();
}
protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
public bool CanSimulateBug
{
get
{
return _canSimulateBug;
}
set
{
_canSimulateBug = value;
RaisePropertyChanged();
}
}
public string Status
{
get
{
return _status;
}
set
{
_status = value ?? string.Empty;
RaisePropertyChanged();
}
}
public int SelectedTab
{
get
{
return _selectedTab;
}
set
{
UpdateStatus($"SelectedTab changing... Value={value}");
if (this.CanSimulateBug)
{
SimulateBug(value);
}
_selectedTab = value;
UpdateStatus($"SelectedTab changed. Value={value}");
// This missing line was added as per
Felix's comment
RaisePropertyChanged();
}
}
private void UpdateStatus(string message)
{
var formattedMessage = $"[{Thread.CurrentThread.ManagedThreadId}] {DateTime.Now.ToLongTimeString()}: {message}";
this.Status = formattedMessage;
Debug.WriteLine(formattedMessage);
}
private void SimulateBug(int id)
{
var delay = TimeSpan.FromSeconds(3);
UpdateStatus($"Bug simulation started... ID={id}, Delay={delay}");
// IMPORTANT: If you comment out this following line
// ... the application will behave as expected.
Application.Current.Dispatcher.Invoke( // blocking call
DispatcherPriority.Background, // tells UI thread to execute this as lowest priority job
new Action(delegate { /* do nothing */ }));
Thread.Sleep(delay);
UpdateStatus($"Bug simulation complete. ID={id}");
}
}
您的完整代码正在单个线程上执行。您不能使用单个线程执行并发操作。您当前正在做的是通过同步调用两个潜在的 long-running 操作来阻塞主线程两次(太长):
- 同步
Dispatcher
调用使用 Dispatcher.Invoke
:
Application.Current.Dispatcher.Invoke(() => {}, DispatcherPriority.Background);
- 同步线程休眠:
Thread.Sleep(TimeSpan.FromSeconds(3));
在执行这些同步操作时,主线程不能自由执行管理托管项目的选择状态。
通过等待 Dispatcher
到 return 然后将其发送到睡眠状态,即挂起它,主线程被阻塞。
因此,Selector
无法取消选择之前选择的 TabItem
。
Selector
能够取消选择过程,这涉及处理所选项目并取消选择所有其他项目(如果不支持 multi-select)。很明显,Selector
取消了pending项的unselection/processing。
您应该能够通过监听附加到 TabItem
的 Selector.Unselected
事件来对此进行测试。它不应该被提高。显然,主线程的阻塞为 Selector
.
的内部项验证创建了竞争条件
要解决此竞争条件,将排队的调度程序消息的 DispatcherPriority
增加到至少 DispatcherPriority.DataBind
(高于 DispatcherPriority.Input
)应该就足够了:
Application.Current.Dispatcher.Invoke(() => {}, DispatcherPriority.DataBind);
这不是推荐的修复方法,尽管它修复了竞争条件,因此修复了多个选定选项卡的问题,因为关键代码现在可以及时执行。真正的潜在问题是阻塞的主线程(实际上是阻塞的Dispatcher
)。
你永远不想阻塞主线程。现在你明白为什么了。
为此,.NET 引入了 TPL。此外,compiler/runtime 环境允许真正的异步执行:通过将执行委托给 OS,.NET 可以使用中断等内核级功能。这样 .NET 可以允许主线程继续,例如,处理基本的 UI 相关事件,如设备输入。
OS 级别和框架级别之间的部分接口是 Dispatcher
和 InputManager
。 Dispatcher
基本上管理关联的线程。在 STA 应用程序中,这是主线程。现在,当您使用 Thread.Sleep
阻塞主线程时,Dispatcher
无法继续处理包含在关联的调度程序线程(主线程)上执行的处理程序的消息队列。
Dispatcher
现在无法执行 InputManager
发布的输入事件。由于依赖 属性 系统(路由事件和数据绑定引擎基于该系统)和通常 UI 控件的代码也在 Dispatcher
上执行,它们也被挂起。
极低的调度程序优先级 DispatcherPriority.Background
与长 Thread.Sleep
相结合使问题更加严重。
解决方法是不阻塞主线程:
- Post 异步处理
Dispatcher
并允许应用程序在作业排队和等待时通过调用 Dispatcher.InvokeAsync
: 继续
Application.Current.Dispatcher.InvokeAsync(() => {}, DispatcherPriority.Background);
- 使用
async
/await
: 异步执行阻塞 I/O 绑定操作
await Task.Delay(TimeSpan.FromSeconds(3));
- 并发执行阻塞CPU绑定操作:
Task.Run(() => {});
您的固定代码如下所示:
private async Task SimulateNoBugAsync(int id)
{
var delay = TimeSpan.FromSeconds(3);
UpdateStatus($"Bug simulation started... ID={id}, Delay={delay}");
// If you need to wait for a result or for completion in general,
// await the Dispatcher.InvokeAsync call.
Application.Current.Dispatcher.InvokeAsync(() => {}, DispatcherPriority.Background);
await Task.Delay(delay);
UpdateStatus($"Bug simulation complete. ID={id}");
}
据我了解,WPF“消息”(例如按钮单击处理程序)被添加到内部优先级队列中。然后,单个 UI 线程负责处理排队的消息。
遗憾的是,我对 WPF 的了解还不够深入,无法理解该框架的内部工作原理。所以我的问题是,鉴于只有 1 个线程处理消息...
- 导致选项卡变得无响应的内部事件顺序(高级)是什么?
观察到的行为
- 如果缓慢单击,
TabControl
会按预期运行。- 要重现:每 4 秒单击 1 个选项卡。
- 看来,如果您给
TabControl.SelectedIndex
数据绑定一个完成的机会,控件将按设计运行。
- 如果快速单击选项卡,某些选项卡将变得无响应。
- 重现:在 3 秒内点击尽可能多的标签。
补充阅读
- Two selected tabs in tabcontroller
- 虽然行为相似,但本文有所不同,因为症状是使用
Tab
+MessageBox
的结果。
- 虽然行为相似,但本文有所不同,因为症状是使用
示例代码
以下代码可用于重现该行为,从而永久选中 WPF 选项卡。
粘贴到MainWindow.xaml
:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="30" />
<RowDefinition Height="*" />
<RowDefinition Height="30" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<WrapPanel Grid.Row="0">
<TextBlock>
1. Click on as many tabs as possible within 3 seconds.<LineBreak/>
2. Wait until multiple tabs are selected.<LineBreak/>
3. Uncheck the `Simulate Bug` checkbox.<LineBreak/>
</TextBlock>
</WrapPanel>
<CheckBox Grid.Row="1" IsChecked="{Binding CanSimulateBug}" Content="Simulate Bug"/>
<TabControl x:Name="ColorWorkspaces" Grid.Row="2" SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
<TabItem x:Name="RedTab" Header="Red"/>
<TabItem x:Name="OrangeTab" Header="Orange"/>
<TabItem x:Name="YellowTab" Header="Yellow"/>
<TabItem x:Name="GreenTab" Header="Green"/>
<TabItem x:Name="BlueTab" Header="Blue"/>
<TabItem x:Name="VioletTab" Header="Violet"/>
</TabControl>
<TextBlock Grid.Row="3" Text="{Binding Status}"/>
</Grid>
粘贴到 MainWindow.xaml.cs
:
public partial class MainWindow : Window, INotifyPropertyChanged
{
private int _selectedTab;
private string _status;
private bool _canSimulateBug;
public MainWindow()
{
this.CanSimulateBug = true;
this.Status = String.Empty;
this.DataContext = this;
InitializeComponent();
}
protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
public bool CanSimulateBug
{
get
{
return _canSimulateBug;
}
set
{
_canSimulateBug = value;
RaisePropertyChanged();
}
}
public string Status
{
get
{
return _status;
}
set
{
_status = value ?? string.Empty;
RaisePropertyChanged();
}
}
public int SelectedTab
{
get
{
return _selectedTab;
}
set
{
UpdateStatus($"SelectedTab changing... Value={value}");
if (this.CanSimulateBug)
{
SimulateBug(value);
}
_selectedTab = value;
UpdateStatus($"SelectedTab changed. Value={value}");
// This missing line was added as per
Felix's comment
RaisePropertyChanged();
}
}
private void UpdateStatus(string message)
{
var formattedMessage = $"[{Thread.CurrentThread.ManagedThreadId}] {DateTime.Now.ToLongTimeString()}: {message}";
this.Status = formattedMessage;
Debug.WriteLine(formattedMessage);
}
private void SimulateBug(int id)
{
var delay = TimeSpan.FromSeconds(3);
UpdateStatus($"Bug simulation started... ID={id}, Delay={delay}");
// IMPORTANT: If you comment out this following line
// ... the application will behave as expected.
Application.Current.Dispatcher.Invoke( // blocking call
DispatcherPriority.Background, // tells UI thread to execute this as lowest priority job
new Action(delegate { /* do nothing */ }));
Thread.Sleep(delay);
UpdateStatus($"Bug simulation complete. ID={id}");
}
}
您的完整代码正在单个线程上执行。您不能使用单个线程执行并发操作。您当前正在做的是通过同步调用两个潜在的 long-running 操作来阻塞主线程两次(太长):
- 同步
Dispatcher
调用使用Dispatcher.Invoke
:
Application.Current.Dispatcher.Invoke(() => {}, DispatcherPriority.Background);
- 同步线程休眠:
Thread.Sleep(TimeSpan.FromSeconds(3));
在执行这些同步操作时,主线程不能自由执行管理托管项目的选择状态。
通过等待 Dispatcher
到 return 然后将其发送到睡眠状态,即挂起它,主线程被阻塞。
因此,Selector
无法取消选择之前选择的 TabItem
。
Selector
能够取消选择过程,这涉及处理所选项目并取消选择所有其他项目(如果不支持 multi-select)。很明显,Selector
取消了pending项的unselection/processing。
您应该能够通过监听附加到 TabItem
的 Selector.Unselected
事件来对此进行测试。它不应该被提高。显然,主线程的阻塞为 Selector
.
的内部项验证创建了竞争条件
要解决此竞争条件,将排队的调度程序消息的 DispatcherPriority
增加到至少 DispatcherPriority.DataBind
(高于 DispatcherPriority.Input
)应该就足够了:
Application.Current.Dispatcher.Invoke(() => {}, DispatcherPriority.DataBind);
这不是推荐的修复方法,尽管它修复了竞争条件,因此修复了多个选定选项卡的问题,因为关键代码现在可以及时执行。真正的潜在问题是阻塞的主线程(实际上是阻塞的Dispatcher
)。
你永远不想阻塞主线程。现在你明白为什么了。
为此,.NET 引入了 TPL。此外,compiler/runtime 环境允许真正的异步执行:通过将执行委托给 OS,.NET 可以使用中断等内核级功能。这样 .NET 可以允许主线程继续,例如,处理基本的 UI 相关事件,如设备输入。
OS 级别和框架级别之间的部分接口是 Dispatcher
和 InputManager
。 Dispatcher
基本上管理关联的线程。在 STA 应用程序中,这是主线程。现在,当您使用 Thread.Sleep
阻塞主线程时,Dispatcher
无法继续处理包含在关联的调度程序线程(主线程)上执行的处理程序的消息队列。
Dispatcher
现在无法执行 InputManager
发布的输入事件。由于依赖 属性 系统(路由事件和数据绑定引擎基于该系统)和通常 UI 控件的代码也在 Dispatcher
上执行,它们也被挂起。
极低的调度程序优先级 DispatcherPriority.Background
与长 Thread.Sleep
相结合使问题更加严重。
解决方法是不阻塞主线程:
- Post 异步处理
Dispatcher
并允许应用程序在作业排队和等待时通过调用Dispatcher.InvokeAsync
: 继续
Application.Current.Dispatcher.InvokeAsync(() => {}, DispatcherPriority.Background);
- 使用
async
/await
: 异步执行阻塞 I/O 绑定操作
await Task.Delay(TimeSpan.FromSeconds(3));
- 并发执行阻塞CPU绑定操作:
Task.Run(() => {});
您的固定代码如下所示:
private async Task SimulateNoBugAsync(int id)
{
var delay = TimeSpan.FromSeconds(3);
UpdateStatus($"Bug simulation started... ID={id}, Delay={delay}");
// If you need to wait for a result or for completion in general,
// await the Dispatcher.InvokeAsync call.
Application.Current.Dispatcher.InvokeAsync(() => {}, DispatcherPriority.Background);
await Task.Delay(delay);
UpdateStatus($"Bug simulation complete. ID={id}");
}