WPF 应用启动过程同时启动多个 UI 线程且访问 ContentPresenter 可能让多个 UI 线程互等
在应用启动过程里,除了主 UI 线程之外,如果还多启动了新的 UI 线程,且此新的 UI 线程碰到 ContentPresenter 类型,那么将可能存在让新的 UI 线程和主 UI 线程互等。这是多线程安全问题,不是很好复现,即使采用 demo 的代码,也需要几千次运行才能在某些配置比较差的机器上遇到新的 UI 线程和主 UI 线程互等,应用启动失败。本文来告诉大家复现的步骤,以及原因,和解决方法
复现步骤
只需要在主 UI 线程里,加载的资源里面包含 ContentPresenter 类型的初始化。然后在主 UI 线程执行 App 时,同时启动另一个 UI 线程,让另一个 UI 线程碰到 ContentPresenter 类型。碰到 ContentPresenter 类型,让 ContentPresenter 类型的静态构造函数能被执行,代码如下
先在 App.xaml 定义资源,定义的资源刚好碰到 ContentPresenter 类型
<Application x:Class="BerehenachearbairGarciwereyer.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BerehenachearbairGarciwereyer"
StartupUri="MainWindow.xaml">
<Application.Resources>
<Style x:Key="DefaultButtonStyle" TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Width" Value="100" />
<Setter Property="Height" Value="100" />
<Setter Property="Margin" Value="10,10,10,10" />
<Setter Property="Template" >
<Setter.Value>
<ControlTemplate>
<ContentPresenter Content="123"></ContentPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources>
</Application>
大家都知道,在 WPF 里的 XAML 将会被构建为 BAML 文件,在启动过程里面加载 BAML 将需要调用到 WPF 底层,将 BAML 展开内存。如上代码将需要创建 ContentPresenter 对象
在 App.xaml.cs 里,在 App 构造函数再启动另一个 UI 线程,在新 UI 线程里面访问 ContentPresenter 类型的 ContentProperty 属性,这是一个静态属性,在类型在程序集第一次碰到,将会调用类型的静态构造函数
public partial class App : Application
{
public App()
{
RunNewUIThread();
}
public static void RunNewUIThread()
{
Thread thread = new(Run);
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
void Run()
{
var currentDispatcher =
System.Windows.Threading.Dispatcher.CurrentDispatcher;
currentDispatcher.InvokeAsync(() =>
{
TouchContentPresenter();
});
System.Windows.Threading.Dispatcher.Run();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void TouchContentPresenter()
{
// Just call the .cctor in ContentPresenter.
var property = ContentPresenter.ContentProperty;
CaptureObject(property);
}
private static void CaptureObject(object obj)
{
Debug.WriteLine(obj);
}
}
以上的代码为了不在 Release 下被优化,于是写了 TouchContentPresenter 和 CaptureObject 两个方法。类型的静态构造函数是在类型被碰到之前,放在 TouchContentPresenter 方法里面,可以让代码在准备调用 TouchContentPresenter 方法时才尝试执行 ContentPresenter 的静态构造函数。同时加上 MethodImplOptions.NoInlining 让代码不会被内联
再加上 CaptureObject 方法,强行捕获参数,从而让获取属性的代码不会被优化
复现的代码放在 https://github.com/lindexi/lindexi_gd/tree/de8bdfbf4715c7200631913cecd24749c98228a3/BerehenachearbairGarciwereyer 上,拉下来之后,构建运行,大概运行几千次,预计是可以复现
在复现时,可以看到线程 Id 为 22436 的主 UI 线程在等待 ContentPresenter 的静态构造函数完成,如下图

这是因为在 .NET 里面,一个类型的静态构造函数,只能由一个线程执行,不会存在多线程同时执行静态构造函数。如果有某个线程在执行静态构造函数,那么其他的线程将需要等待静态构造函数执行完成才能继续碰类型。也就是相当于静态构造函数进入时加了锁,需要在执行完成之后才会释放锁,其他的线程都在等待静态构造函数的锁,也就是等待静态构造函数执行完
在线程 Id 为 16100 的新 UI 线程,执行到 ContentPresenter 的静态构造函数,然而静态构造函数在等待一个被主 UI 线程拿到的锁,静态构造函数无法执行完成

原理
核心原因是一个不良设计导致的,在 ContentPresenter 的静态构造函数里面,干的活太多了。其中就包括调用了 CreateTextBlockFactory 等方法,如下代码
static ContentPresenter()
{
DataTemplate template;
FrameworkElementFactory text;
Binding binding;
// Default template for strings when hosted in ContentPresener with RecognizesAccessKey=true
template = new DataTemplate();
text = CreateAccessTextFactory();
text.SetValue(AccessText.TextProperty, new TemplateBindingExtension(ContentProperty));
template.VisualTree = text;
template.Seal();
s_AccessTextTemplate = template;
// Default template for strings
template = new DataTemplate();
text = CreateTextBlockFactory();
text.SetValue(TextBlock.TextProperty, new TemplateBindingExtension(ContentProperty));
template.VisualTree = text;
template.Seal();
s_StringTemplate = template;
// 忽略其他代码
}
internal static FrameworkElementFactory CreateAccessTextFactory()
{
FrameworkElementFactory text = new FrameworkElementFactory(typeof(AccessText));
return text;
}
在 CreateAccessTextFactory 创建的 FrameworkElementFactory 对象的构造函数代码如下,在构造函数将会给 FrameworkElementFactory.Type 属性赋值
public FrameworkElementFactory(Type type, string name)
{
Type = type;
Name = name;
}
然而 FrameworkElementFactory.Type 属性是比较复杂的,在赋值积分啊里面将会调用到 XamlReader.BamlSharedSchemaContext.GetKnownXamlType 方法
public Type Type
{
get { return _type; }
set
{
// 忽略其他代码
// If this is a KnownType in the BamlSchemaContext, then there is a faster way to create
// an instance of that type than using Activator.CreateInstance. So in that case
// save the delegate for later creation.
WpfKnownType knownType = null;
if (_type != null)
{
knownType = XamlReader.BamlSharedSchemaContext.GetKnownXamlType(_type) as WpfKnownType;
}
_knownTypeFactory = (knownType != null) ? knownType.DefaultConstructor : null;
}
}
在 GetKnownXamlType 里面将需要等待 _syncObject 对象的锁。然而 XamlReader.BamlSharedSchemaContext 是一个静态属性,这就意味着在使用此属性,无论是主 UI 线程还是新 UI 线程都拿到相同的 WpfSharedBamlSchemaContext 类型对象,也就是说调用到 WpfSharedBamlSchemaContext 的其他方法时,等待的是相同的一个 _syncObject 对象
internal XamlType GetKnownXamlType(Type type)
{
XamlType xamlType;
lock (_syncObject)
{
// 忽略其他代码
}
return xamlType;
}
如果是在 新 UI 线程先碰到 ContentPresenter 类型,那么 ContentPresenter 的静态构造函数将在 新 UI 线程执行。执行的静态构造函数将会等待 WpfSharedBamlSchemaContext 的 _syncObject 对象的锁。如果刚好主 UI 线程正在展开 Baml 需要使用 Create_BamlProperty_ContentPresenter_ContentSource 方法,那么在此方法进入时,将因为碰到了 ContentPresenter 类型,需要等待 ContentPresenter 的静态构造函数执行完成
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
private WpfKnownMember Create_BamlProperty_ContentPresenter_ContentSource()
{
Type type = typeof(System.Windows.Controls.ContentPresenter);
DependencyProperty dp = System.Windows.Controls.ContentPresenter.ContentSourceProperty;
var bamlMember = new WpfKnownMember( this, // Schema Context
this.GetXamlType(typeof(System.Windows.Controls.ContentPresenter)), // DeclaringType
"ContentSource", // Name
dp, // DependencyProperty
false, // IsReadOnly
false // IsAttachable
);
bamlMember.TypeConverterType = typeof(System.ComponentModel.StringConverter);
bamlMember.Freeze();
return bamlMember;
}
在进入 Create_BamlProperty_ContentPresenter_ContentSource 方法之前,其实主 UI 线程已获取了 _syncObject 对象的锁。也就是说 ContentPresenter 的静态构造函数必须等待主 UI 线程释放锁才能完成,然而主 UI 线程必须等待 ContentPresenter 的静态构造函数执行完成才能释放锁
于是就构成了两个线程相互等待。在主 UI 线程进入 Create_BamlProperty_ContentPresenter_ContentSource 方法,需要等待 ContentPresenter 的静态构造函数执行完成,才能释放主 UI 线程的锁,让 ContentPresenter 的静态构造继续执行。执行在新 UI 线程的 ContentPresenter 的静态构造函数在等待主 UI 线程释放锁才能执行完成。主 UI 线程在等待新 UI 线程的静态构造函数执行完成。新 UI 线程在等待主 UI 线程等待静态构造函数执行完成之后释放的锁
两个 UI 线程进入摸鱼,应用就起不来
看到以上的原理,在实际的应用里面,想要遇到这个坑还是很难。因为 ContentPresenter 的静态构造函数只会执行一次,谁能说一定不在主 UI 线程执行?而且即使在新 UI 线程执行,那也不一定刚好在进入静态构造函数,主 UI 线程也需要用到 ContentPresenter 的相关属性。这个是需要刚好的,如果在主 UI 线程需要用到 ContentPresenter 的相关属性比较前,就在新 UI 线程进入 ContentPresenter 的静态构造函数,那将因为在新 UI 线程能等到锁而成功执行完成 ContentPresenter 的静态构造函数。如果在主 UI 线程碰到 ContentPresenter 的相关属性时,那么此时的 ContentPresenter 的静态构造函数就由主 UI 线程执行,也没有任何问题。只有在主 UI 线程拿到了锁,在准备碰到 ContentPresenter 的上一个方法时,也就是 WpfSharedBamlSchemaContext.CreateKnownMember 方法,此时的主 UI 线程已拿到锁,在新 UI 线程进入 ContentPresenter 的静态构造函数,如此才能让两个线程相互等待
解决方法
了解了原理,解决方法就十分简单了,只需要不让 ContentPresenter 的静态构造方法被新的 UI 线程调度执行即可。在新的 UI 线程执行之前,先碰一下 ContentPresenter 类型即可,例如获取此类型的某个属性之类,如以下代码
[MethodImpl(MethodImplOptions.NoInlining)]
private static void TouchContentPresenter()
{
// Just call the .cctor in ContentPresenter.
var property = ContentPresenter.ContentProperty;
CaptureObject(property);
}
private static void CaptureObject(object obj)
{
Debug.WriteLine(obj);
}
在开启新的 UI 线程之前,先调用一下 TouchContentPresenter 方法即可。由于碰到了类型里面的某个属性,无论是否静态,都会先调用对应的类型的静态构造函数,静态构造函数只会被调用一次,因此即可解决线程安全问题
另一个解决方法是不要尝试在应用启动的过程里面开启多个 UI 线程。在应用启动完成之后,再开启,就基本不会遇到此问题
这个问题已报告给 WPF 官方,详细请看 Multi UI thread visit the ContentPresenter at application startup may deadlock · Issue #6609 · dotnet/wpf
我认为这也是一个设计缺陷,稍微熟悉 .NET 的开发者都知道,在静态构造函数里面碰锁是很危险的。因为静态构造函数的调用是不确定的,取决于第一次碰到此类型的代码进入之前。因此静态构造函数里面的碰锁的时机将是不可预期的。再加上静态构造函数只能被调用一次,这就让其他多线程碰到此类型,都需要等待静态构造函数执行完成。由于静态构造函数的调用是不可预期的,多线程里只有一个线程能进入静态构造函数,其他线程需要等待,于是此等待就相当于一个锁,如果在静态构造函数里面会碰到另一个锁,那就相当于有两个锁。有两个锁加上不可预期的调用,那这个逻辑很好构成相互等待
WPF 应用启动过程同时启动多个 UI 线程且访问 ContentPresenter 可能让多个 UI 线程互等的更多相关文章
- WPF [调用线程无法访问此对象,因为另一个线程拥有该对象。] 解决方案以及如何实现字体颜色的渐变
本文说明WPF [调用线程无法访问此对象,因为另一个线程拥有该对象.] 解决方案以及如何实现字体颜色的渐变 先来看看C#中Timer的简单说明,你想必猜到实现需要用到Timer的相关知识了吧. C# ...
- WPF解决方案------调用线程无法访问此对象,因为另一个线程拥有该对象
WPF [调用线程无法访问此对象,因为另一个线程拥有该对象.] 解决方案 在这里以播放图片为例进行说明,代码如下: void _Timer_Elapsed(object sender, Elapsed ...
- Linux系统在启动过程中启动级别发生错误的解决办法
一.系统启动级别一共有六个: 0:系统停机模式,系统不可以正常启动 1:单用户模式, root权限,用于系统的维护,禁止远程登陆 2:多用户模式,没有NFS网络支持 3:完整的多用户文本模式,有NFS ...
- 调用线程无法访问此对象,因为另一个线程拥有该对象 [c# wpf定时器程序报的错误]
WPF:Dispatcher.Invoke 方法,只有在其上创建 Dispatcher 的线程才可以直接访问DispatcherObject.若要从不同于在其上创建 DispatcherObject ...
- WPF 出现“调用线程无法访问此对象,因为另一个线程拥有该对象”
引起这种错误多半是由于在非UI线程刷新界面,解决此问题可以使用Dispatcher this.Dispatcher.Invoke(new Action(() => { UpdateUI(stri ...
- [原] KVM 虚拟化原理探究(2)— QEMU启动过程
KVM 虚拟化原理探究- QEMU启动过程 标签(空格分隔): KVM [TOC] 虚拟机启动过程 第一步,获取到kvm句柄 kvmfd = open("/dev/kvm", O_ ...
- Android源码——Activity组件的启动过程
根Activity启动过程 Launcher启动MainActivity的过程主要分为6个步骤: 一.Launcher向ActivityManagerService发送一个启动MainActivity ...
- Spark 启动过程(standalone)
Spark启动过程 正常启动Spark集群时往往使用start-all.sh ,此脚本中通过调用start-master.sh和start-slaves.sh启动mater及workers节点. 1. ...
- HDFS Namenode启动过程
文章作者:luxianghao 文章来源:http://www.cnblogs.com/luxianghao/p/6564032.html 转载请注明,谢谢合作. 免责声明:文章内容仅代表个人观点, ...
- Linux 启动过程详解
目录 1. Linux启动过程 2. 启动过程概述 3. 引导加载阶段 4. 内核阶段 4.1 内核加载阶段 4.2 内核启动阶段 5. 早期的用户空间 6. 初始化过程 6.1 SysV init ...
随机推荐
- Advanced .Net Debugging 5:基本调试任务(线程的操作、代码审查、CLR内部的命令、诊断命令和崩溃转储文件)
一.介绍 这是我的<Advanced .Net Debugging>这个系列的第五篇文章.今天这篇文章的标题虽然叫做"基本调试任务",但是这章的内容还是挺多的.上一篇我 ...
- C# 优雅的处理TCP数据(心跳,超时,粘包断包,SSL加密 ,数据处理等)
Tcp是一个面向连接的流数据传输协议,用人话说就是传输是一个已经建立好连接的管道,数据都在管道里像流水一样流淌到对端.那么数据必然存在几个问题,比如数据如何持续的读取,数据包的边界等. Nagle's ...
- KingbaseES 原生XML系列二 -- XML数据操作函数
KingbaseES 原生XML系列二--XML数据操作函数(DELETEXML,APPENDCHILDXML,INSERTCHILDXML,INSERTCHILDXMLAFTER,INSERTCHI ...
- 第十三届蓝桥杯大赛软件赛省赛【Java 大学B 组】试题B: 山
1 public class HelloWorld { 2 public static void main(String args[]) { 3 long count=0; 4 String temp ...
- #贪心#洛谷 3173 [HAOI2009]巧克力
题目 分析 既然每一刀都要切,那肯定代价越大的要越早切, 考虑按代价降序排序,如果切了一行,求切列的时候贡献的行数就多了1. 代码 #include <cstdio> #include & ...
- Python设计模式----2.工厂模式
工厂方法模式是简单工厂模式的衍生,解决了许多简单工厂模式的问题 首先完全实现'开-闭 原则',实现了可扩展.其次更复杂的层次结构,可以应用于产品结果复杂的场合. 工厂方法模式的对简单工厂模式进行了抽象 ...
- Prometheus AlertManager 生产实践-直接根据 to_email label 发 alert 到对应邮箱
概述 通过之前的文章 - Prometheus Alertmanager 生产配置趟过的坑总结, 我们已经知道 AlertManager 作为告警平台,是非常强大的,可以去重 (deduplicati ...
- Thymeleaf SSTI模板注入分析
环境搭建 先搭建一个SpringMVC项目,参考这篇文章,或者参考我以前的spring内存马分析那篇文章 https://blog.csdn.net/weixin_65287123/article/d ...
- iOS系统崩溃的捕获
iOS系统崩溃的捕获 相信大家在开发iOS程序的时候肯定写过各种Bug,而其中最为严重的Bug就是会导致崩溃的Bug(一般来说妥妥的P1级).在应用软件大大小小的各种异常中,崩溃确实是最让人难以接受的 ...
- SilentEye qsnctf wp
题目附件(注:文件名为Luminous.jpg) 根据题目提示,使用SilentEye工具 将图片使用SilentEye打开 使用左下角的Decode解密功能 猜测密码为文件名,输入并开始解密 将被加 ...