Skip to content

dotnet 读 WPF 源代码笔记 了解 WPF 已知问题 后台线程创建 WriteableBitmap 锁住主线程

Updated: at 08:22,Created: at 08:53

在 WPF 中,如果在没有开启 Dispatcher 的后台线程里面创建 WriteableBitmap 对象,在 WriteableBitmap 构造函数传入在主线程创建的 BitmapSource 也许就会锁住主线程。本文将通过 WPF 框架源代码告诉大家为什么会锁住主线程

这是在 WPF 开源仓库上一个小伙伴报的,详细请看 WriteableBitmap hangs when source bitmap is rendered on other thread · Issue #4396 · dotnet/wpf

复现步骤十分简单,只需要在后台线程创建完成一个 BitmapSource 分别传入给主线程显示和后台线程创建 WriteableBitmap 就会锁住主线程,最简单的代码如下

Task.Run(() =>
{
var image = new BitmapImage(new Uri(fileName));
image.Freeze(); // locks the bitmap source, so other threads can access
Dispatcher.InvokeAsync(() => Image.Source = (BitmapSource) image);
//Thread.Sleep(10); // WPF needs time to render the bitmap. During this period, creating a WriteableBitmap makes the program hang.
_ = new WriteableBitmap(image);
});

上面代码的 Image 是一个在 XAML 定义的控件

<Image x:Name="Image"/>

上面的 fileName 是一个文件的路径。详细的测试代码请看 https://github.com/SetTrend/BitmapSourceTest

为什么这个后台线程和主线程会相互等待?原因是在后台线程创建 WriteableBitmap 时,会进入 WriteableBitmap.InitFromBitmapSource 方法,在这个方法里面获取了一个主线程后续将会等待的锁。然而后台线程后续需要等待主线程返回,才能完成创建图片,因此主线程在等待后台线程的锁而后台线程在等待主线程返回,两个线程在等待

通过 WPF 仓库的源代码可以看到 WriteableBitmap.InitFromBitmapSource 方法的实现如下

public sealed class WriteableBitmap : BitmapSource
{
private void InitFromBitmapSource(
BitmapSource source
)
{
// Ignore code
_syncObject = source.SyncObject;
lock (_syncObject)
{
// Ignore code
}
}
}

也就是说在后台线程将会拿到创建 WriteableBitmap 构造函数传入的 BitmapSource 的 SyncObject 对象作为锁。对应测试代码的 image 变量的 SyncObject 对象先被后台线程获取,然后在主线程渲染时,也需要用到这个锁,在主线程的堆栈如下

PresentationCore.dll!System.Windows.Media.Imaging.BitmapSource.UpdateBitmapSourceResource(System.Windows.Media.Composition.DUCE.Channel channel = {System.Windows.Media.Composition.DUCE.Channel}, bool skipOnChannelCheck)
PresentationCore.dll!System.Windows.Media.Imaging.BitmapSource.UpdateResource(System.Windows.Media.Composition.DUCE.Channel channel, bool skipOnChannelCheck)
PresentationCore.dll!System.Windows.Media.Imaging.BitmapSource.AddRefOnChannelCore(System.Windows.Media.Composition.DUCE.Channel channel = {System.Windows.Media.Composition.DUCE.Channel})
PresentationCore.dll!System.Windows.Media.Imaging.BitmapSource.System.Windows.Media.Composition.DUCE.IResource.AddRefOnChannel(System.Windows.Media.Composition.DUCE.Channel channel)
PresentationCore.dll!System.Windows.Media.RenderData.System.Windows.Media.Composition.DUCE.IResource.AddRefOnChannel(System.Windows.Media.Composition.DUCE.Channel channel = {System.Windows.Media.Composition.DUCE.Channel})
PresentationCore.dll!System.Windows.UIElement.RenderContent(System.Windows.Media.RenderContext ctx, bool isOnChannel)
PresentationCore.dll!System.Windows.Media.Visual.UpdateContent(System.Windows.Media.RenderContext ctx = {System.Windows.Media.RenderContext}, System.Windows.Media.VisualProxyFlags flags, bool isOnChannel)
PresentationCore.dll!System.Windows.Media.Visual.RenderRecursive(System.Windows.Media.RenderContext ctx = {System.Windows.Media.RenderContext})

在主线程渲染图片,需要在 BitmapSource.UpdateBitmapSourceResource 方法里面获取锁,请看代码

public abstract class BitmapSource : ImageSource, DUCE.IResource
{
internal virtual void UpdateBitmapSourceResource(DUCE.Channel channel, bool skipOnChannelCheck)
{
// Ignore code
// We may end up loading in the bitmap bits so it's necessary to take the sync lock here.
lock (_syncObject)
{
channel.SendCommandBitmapSource(
_duceResource.GetHandle(channel),
DUCECompatiblePtr
);
}
}
}
}

上面代码的 _syncObject 和在后台线程获取的 SyncObject 是相同的对象,因此主线程需要等待后台线程。但是后台线程在执行到 MediaSystem.Startup 方法时,就需要等待主线程返回,后台线程调用堆栈如下

[Manage to Native]
PresentationCore.dll!System.Windows.Media.MediaSystem.Startup(System.Windows.Media.MediaContext mc = {System.Windows.Media.MediaContext})
PresentationCore.dll!System.Windows.Media.MediaContext.MediaContext(System.Windows.Threading.Dispatcher dispatcher = {System.Windows.Threading.Dispatcher})
PresentationCore.dll!System.Windows.Media.MediaContext.From(System.Windows.Threading.Dispatcher dispatcher)
PresentationCore.dll!System.Windows.Media.Imaging.WriteableBitmap.SubscribeToCommittingBatch()
PresentationCore.dll!System.Windows.Media.Imaging.WriteableBitmap.Unlock()
PresentationCore.dll!System.Windows.Media.Imaging.WriteableBitmap.InitFromBitmapSource(System.Windows.Media.Imaging.BitmapSource source)
PresentationCore.dll!System.Windows.Media.Imaging.WriteableBitmap.WriteableBitmap(System.Windows.Media.Imaging.BitmapSource source)
> BitmapSourceTest.dll!BitmapSourceTest.MainWindow.ProcessImageAsync(string filePath)
BitmapSourceTest.dll!BitmapSourceTest.MainWindow.BrowseFile_Click.AnonymousMethod__0()
System.Private.CoreLib.dll!System.Threading.Tasks.Task.InnerInvoke()
System.Private.CoreLib.dll!System.Threading.Tasks.Task..cctor.AnonymousMethod__277_0(object obj)
System.Private.CoreLib.dll!System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread threadPoolThread = {System.Threading.Thread}, System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)
System.Private.CoreLib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot = Id = 189, Status = Running, Method = "Void <BrowseFile_Click>b__0()", System.Threading.Thread threadPoolThread)
System.Private.CoreLib.dll!System.Threading.Tasks.Task.ExecuteEntryUnsafe(System.Threading.Thread threadPoolThread)
System.Private.CoreLib.dll!System.Threading.Tasks.Task.ExecuteFromThreadPool(System.Threading.Thread threadPoolThread)
System.Private.CoreLib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

可以从上面代码看到,主线程在等待后台线程的锁,而后台线程需要等待主线程返回才能释放锁

其实在后台线程创建图片,同时创建的图片的参数还是在主线程使用的图片,这样的逻辑不多,更多使用的是只在后台线程创建图片然后通过 Freeze 给到主线程用来解决性能问题。但上面测试代码的逻辑也不算出错,可以算 WPF 的已知坑。也许我会尝试去修复这个问题

如果不更改 WPF 框架代码,那么一个尝试解决的方法是在后台线程开启 UI 线程,预热一下渲染。预热用来解决后台线程创建 MediaContext 需要等待主线程,通过预先创建,此时可以等待到主线程,如下面代码

Dispatcher backgroundDispatcher = null!;
AutoResetEvent resetEvent = new AutoResetEvent(false);
Thread thread = new Thread(() =>
{
backgroundDispatcher = Dispatcher.CurrentDispatcher;
resetEvent.Set();
Dispatcher.Run();
});
thread.SetApartmentState(ApartmentState.STA);
thread.IsBackground = true;
thread.Start();
resetEvent.WaitOne();
// To Create the MediaContext which is thread static
backgroundDispatcher.InvokeAsync(() => new WriteableBitmap(1, 1, 96, 96, PixelFormats.Bgr32, null));
backgroundDispatcher.InvokeAsync(() =>
{
var image = new BitmapImage(new Uri(openDialog.FileName));
image.Freeze(); // locks the bitmap source, so other threads can access
Dispatcher.InvokeAsync(() => Image.Source = (BitmapSource) image);
_ = new WriteableBitmap(image);
});

代码放在 github 欢迎小伙伴访问

此问题已被我修复,请看 https://github.com/dotnet/wpf/pull/4425

当前的 WPF 在 https://github.com/dotnet/wpf 完全开源,使用友好的 MIT 协议,意味着允许任何人任何组织和企业任意处置,包括使用,复制,修改,合并,发表,分发,再授权,或者销售。在仓库里面包含了完全的构建逻辑,只需要本地的网络足够好(因为需要下载一堆构建工具),即可进行本地构建


知识共享许可协议

原文链接: http://blog.lindexi.com/post/dotnet-%E8%AF%BB-WPF-%E6%BA%90%E4%BB%A3%E7%A0%81%E7%AC%94%E8%AE%B0-%E4%BA%86%E8%A7%A3-WPF-%E5%B7%B2%E7%9F%A5%E9%97%AE%E9%A2%98-%E5%90%8E%E5%8F%B0%E7%BA%BF%E7%A8%8B%E5%88%9B%E5%BB%BA-WriteableBitmap-%E9%94%81%E4%BD%8F%E4%B8%BB%E7%BA%BF%E7%A8%8B

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。 欢迎转载、使用、重新发布,但务必保留文章署名 林德熙 (包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我 联系