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

在 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://blog.lindexi.com/ 欢迎大家访问,里面有很多新的博客。只有在我看到博客写成熟之后才会放在csdn或博客园,但是一旦发布了就不再更新

如果在博客看到有任何不懂的,欢迎交流,我搭建了 dotnet 职业技术学院 欢迎大家加入

如有不方便在博客评论的问题,可以加我 QQ 2844808902 交流

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

lindexi_gd CSDN认证博客专家 C# WPF UWP 微软最具价值专家
我是微软Windows应用开发方向的最具价值专家,欢迎访问我博客blog.lindexi.com里面有大量WPF和UWP博客
相关推荐
<p> <strong><span style="font-size:20px;color:#FF0000;">本课程要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者</span></strong> </p> <p> <span style="color:#FF0000;"><strong><span style="font-size:18px;">1. 包含:<span style="color:#FFFF00;background-color:#FF0000;">项目源码、</span><span style="color:#FFFF00;background-color:#FF0000;">项目文档、数据库脚本、软件工具</span>等所有资料</span></strong></span> </p> <p> <span style="color:#FF0000;"><strong><span style="font-size:18px;">2. 手把手的带你从零开始部署运行本套系统</span></strong></span> </p> <p> <span style="color:#FF0000;"><strong><span style="font-size:18px;">3. 该项目附带的源码资料可作为毕设使用</span></strong></span> </p> <p> <span style="color:#FF0000;"><strong><span style="font-size:18px;">4. 提供技术答疑和远程协助指导</span></strong></span><strong><span style="font-size:18px;"></span></strong> </p> <p> <br /> </p> <p> <span style="font-size:18px;"><strong>项目运行截图:</strong></span> </p> <p> <strong><span style="font-size:18px;">1)系统登陆界面</span></strong> </p> <p> <strong><span style="font-size:18px;"><img src="https://img-bss.csdn.net/202002241015433522.png" alt="" /><br /> </span></strong> </p> <p> <strong><span style="font-size:18px;"><strong><span style="font-size:18px;">2)学生模块</span></strong></span></strong> </p> <p> <strong><span style="font-size:18px;"><img src="https://img-bss.csdn.net/202002241015575966.png" alt="" /></span></strong> </p> <p> <strong><span style="font-size:18px;"><strong><span style="font-size:18px;">3)教师模块</span></strong></span></strong> </p> <p> <strong><span style="font-size:18px;"><img src="https://img-bss.csdn.net/202002241016127898.png" alt="" /></span></strong> </p> <p> <strong><span style="font-size:18px;"><strong><span style="font-size:18px;">4)系统管理员</span></strong></span></strong> </p> <p> <strong><span style="font-size:18px;"><img src="https://img-bss.csdn.net/202002241016281177.png" alt="" /></span></strong> </p> <p> <strong><span style="font-size:18px;"><img src="https://img-bss.csdn.net/202002241016369884.png" alt="" /></span></strong> </p> <p> <strong><span style="font-size:18px;"><br /> </span></strong> </p> <p> <strong><span style="font-size:18px;"><strong><span style="font-size:18px;">更多Java毕设项目请关注我的毕设系列课程 <a href="https://edu.csdn.net/lecturer/2104">https://edu.csdn.net/lecturer/2104</a></span></strong></span></strong> </p> <p> <strong><span style="font-size:18px;"><br /> </span></strong> </p>
<p> 课程演示环境:Windows10  </p> <p> 需要学习<span>Ubuntus</span>系统<span>YOLOv4-tiny</span>的同学请前往《<span>YOLOv4-tiny</span>目标检测实战:训练自己的数据集》 <span></span> </p> <p> <span> </span> </p> <p> <span style="color:#E53333;">YOLOv4-tiny</span><span style="color:#E53333;">来了!速度大幅提升!</span><span></span> </p> <p> <span> </span> </p> <p> <span>YOLOv4-tiny</span>在<span>COCO</span>上的性能可达到:<span>40.2% AP50, 371 FPS (GTX 1080 Ti)</span>。相较于<span>YOLOv3-tiny</span>,<span>AP</span>和<span>FPS</span>的性能有巨大提升。并且,<span>YOLOv4-tiny</span>的权重文件只有<span>23MB</span>,适合在移动端、嵌入式设备、边缘计算设备上部署。<span></span> </p> <p> <span> </span> </p> <p> 本课程将手把手地教大家使用<span>labelImg</span>标注和使用<span>YOLOv4-tiny</span>训练自己的数据集。课程实战分为两个项目:单目标检测(足球目标检测)和多目标检测(足球和梅西同时检测)。<span></span> </p> <p> <span> </span> </p> <p> 本课程的<span>YOLOv4-tiny</span>使用<span>AlexAB/darknet</span>,在<span>Windows10</span>系统上做项目演示。包括:<span>YOLOv4-tiny</span>的网络结构、安装<span>YOLOv4-tiny</span>、标注自己的数据集、整理自己的数据集、修改配置文件、训练自己的数据集、测试训练出的网络模型、性能统计<span>(mAP</span>计算<span>)</span>和先验框聚类分析。 <span> </span> </p> <p> <span> </span> </p> <p> 除本课程《<span>Windows</span>版<span>YOLOv4-tiny</span>目标检测实战:训练自己的数据集》外,本人推出了有关<span>YOLOv4</span>目标检测的系列课程。请持续关注该系列的其它视频课程,包括:<span></span> </p> <p> 《<span>Windows</span>版<span>YOLOv4</span>目标检测实战:训练自己的数据集》<span></span> </p> <p> 《<span>Windows</span>版<span>YOLOv4</span>目标检测实战:人脸口罩佩戴识别》<span></span> </p> <p> 《<span>Windows</span>版<span>YOLOv4</span>目标检测实战:中国交通标志识别》<span></span> </p> <p> 《<span>Windows</span>版<span>YOLOv4</span>目标检测:原理与源码解析》<span></span> </p> <p> <span> <img alt="" src="https://img-bss.csdnimg.cn/202007061503586145.jpg" /></span> </p> <p> <span><img alt="" src="https://img-bss.csdnimg.cn/202007061504169339.jpg" /><br /> </span> </p>
©️2020 CSDN 皮肤主题: 终极编程指南 设计师:CSDN官方博客 返回首页