WPF 警惕 StylusPlugIn 的多線程安全問題
在 WPF 里面,即使是相同的觸摸 Id 值,都可能分別在觸摸線程或 UI 線程調入,由于多線程調度問題,可能發生觸摸事件是先 Up 后 Down 的情況
為什么在 WPF 里面,可能在 UI 線程將觸摸消息調入 StylusPlugIn 里面?復現步驟是什么?
本質的機制問題是 StylusPlugIn 附加到 UIElement 上,觸摸線程和 UI 線程之間無法完全實時同步狀態,如當 UI 線程執行布局等邏輯時,將會影響觸摸線程嘗試命中的結果,直接可能就是原本應該命中到給定 UIElement 上的點沒有真的命中到。為了解決此問題,在 WPF 里面添加了兜底實現邏輯,那就是在 UI 線程判斷當前消息是否已經通過 StylusPlugIn 引發了,如果未引發,則補充引發。如此就能規避丟點或不成對 Down Up 的問題
但此兜底邏輯卻帶來了線程安全問題。這是因為 UI 線程判斷消息是否引發是通過時機來決定,如果剛好此時觸摸線程沒有被 CPU 調度,讓 UI 線程更早執行。則 UI 線程判定 StylusPlugIn 沒有引發事件,由 UI 線程執行觸發。盡管在 WPF 框架層確保相同的事件不會重復在 UI 和觸摸線程同時引發,但從原理上無法保證 Down 和 Up 的順序
其線程安全表現就是可能在 StylusPlugIn 收到先 Move 后 Up 再 Down 的情況。其中 Move 和 Up 在一個線程引發,而 Down 在另一個線程引發
復現步驟很簡單,只需要讓 StylusPlugIn 足夠卡頓即可,最簡復現代碼就是隨意編寫一個類型繼承 StylusPlugIn 類,在 OnStylusDown\Move\Up 方法使用 Thread.Sleep 模擬卡頓即可。我將最簡復現 Demo 代碼放在本文末尾
再進一步,通過具體的源代碼了解此問題復現機制原理
如下圖所示,此時觸摸消息進入 OnStylusDown 方法的是主線程

一路追蹤堆棧,可見這是從主線程的鼠標消息進入觸發的,如下圖所示

其調用堆棧如下
LoqairjaniferNudalcefinay.dll!LoqairjaniferNudalcefinay.MainWindowStylusPlugIn.OnStylusUp(System.Windows.Input.StylusPlugIns.RawStylusInput rawStylusInput = {System.Windows.Input.StylusPlugIns.RawStylusInput}) 行 87 C#
PresentationCore.dll!System.Windows.Input.StylusPlugIns.StylusPlugIn.RawStylusInput(System.Windows.Input.StylusPlugIns.RawStylusInput rawStylusInput) 行 107 C#
PresentationCore.dll!System.Windows.Input.StylusPlugIns.StylusPlugInCollection.FireRawStylusInput.AnonymousMethod__0() 行 372 C#
PresentationCore.dll!System.Windows.Input.StylusPlugIns.StylusPlugInCollection.ExecuteWithPotentialLock(System.Action action) 行 478 C#
PresentationCore.dll!System.Windows.Input.StylusPlugIns.StylusPlugInCollection.FireRawStylusInput(System.Windows.Input.StylusPlugIns.RawStylusInput args = {System.Windows.Input.StylusPlugIns.RawStylusInput}) 行 370 C#
PresentationCore.dll!System.Windows.Input.StylusWisp.WispLogic.VerifyStylusPlugInCollectionTarget(System.Windows.Input.RawStylusInputReport rawStylusInputReport = {System.Windows.Input.RawStylusInputReport}) 行 2719 C#
PresentationCore.dll!System.Windows.Input.StylusWisp.WispLogic.PreNotifyInput(object sender, System.Windows.Input.NotifyInputEventArgs e = {System.Windows.Input.NotifyInputEventArgs}) 行 1049 C#
PresentationCore.dll!System.Windows.Input.InputManager.ProcessStagingArea() 行 753 C#
PresentationCore.dll!System.Windows.Input.InputManager.ProcessInput(System.Windows.Input.InputEventArgs input) 行 552 C#
PresentationCore.dll!System.Windows.Input.StylusWisp.WispLogic.InputManagerProcessInput(object oInput) 行 377 C#
WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.InternalRealCall(System.Delegate callback, object args, int numArgs) 未知
WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.TryCatchWhen(object source = {System.Windows.Threading.Dispatcher}, System.Delegate callback, object args, int numArgs, System.Delegate catchHandler = null) 未知
WindowsBase.dll!System.Windows.Threading.DispatcherOperation.InvokeImpl() 未知
WindowsBase.dll!System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(object state) 未知
WindowsBase.dll!MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(object obj) 未知
System.Private.CoreLib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state) 行 137 C#
WindowsBase.dll!MS.Internal.CulturePreservingExecutionContext.Run(MS.Internal.CulturePreservingExecutionContext executionContext = {MS.Internal.CulturePreservingExecutionContext}, System.Threading.ContextCallback callback, object state) 未知
WindowsBase.dll!System.Windows.Threading.DispatcherOperation.Invoke() 未知
WindowsBase.dll!System.Windows.Threading.Dispatcher.ProcessQueue() 未知
WindowsBase.dll!System.Windows.Threading.Dispatcher.WndProcHook(System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) 未知
WindowsBase.dll!MS.Win32.HwndWrapper.WndProc(System.IntPtr hwnd = 0x04190b72, int msg, System.IntPtr wParam = 0x00000000, System.IntPtr lParam = 0x00000000, ref bool handled = false) 未知
WindowsBase.dll!MS.Win32.HwndSubclass.DispatcherCallbackOperation(object o) 未知
WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.InternalRealCall(System.Delegate callback, object args, int numArgs) 未知
WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.TryCatchWhen(object source = {System.Windows.Threading.Dispatcher}, System.Delegate callback, object args, int numArgs, System.Delegate catchHandler = null) 未知
WindowsBase.dll!System.Windows.Threading.Dispatcher.LegacyInvokeImpl(System.Windows.Threading.DispatcherPriority priority, System.TimeSpan timeout, System.Delegate method, object args, int numArgs) 未知
WindowsBase.dll!MS.Win32.HwndSubclass.SubclassWndProc(System.IntPtr hwnd = 0x04190b72, int msg, System.IntPtr wParam = 0x00000000, System.IntPtr lParam = 0x00000000) 未知
[本機到托管的轉換]
[托管到本機的轉換]
WindowsBase.dll!System.Windows.Threading.Dispatcher.PushFrameImpl(System.Windows.Threading.DispatcherFrame frame = {System.Windows.Threading.DispatcherFrame}) 未知
WindowsBase.dll!System.Windows.Threading.Dispatcher.PushFrame(System.Windows.Threading.DispatcherFrame frame) 未知
WindowsBase.dll!System.Windows.Threading.Dispatcher.Run() 未知
PresentationFramework.dll!System.Windows.Application.RunDispatcher(object ignore) 未知
PresentationFramework.dll!System.Windows.Application.RunInternal(System.Windows.Window window) 未知
PresentationFramework.dll!System.Windows.Application.Run() 未知
LoqairjaniferNudalcefinay.dll!LoqairjaniferNudalcefinay.App.Main() 未知
為什么正常情況下,不會有 UI 線程調入呢?這是因為在 WispLogic.VerifyStylusPlugInCollectionTarget 里面的 sendRawStylusInput 局部變量不為 true 值。什么時候 sendRawStylusInput 會為 true 值導致 UI 線程兜底邏輯執行觸發 StylusPlugIn 的方法?如以下有刪減的代碼所示
private void VerifyStylusPlugInCollectionTarget(RawStylusInputReport rawStylusInputReport)
{
StylusPlugInCollection targetPIC = null;
UIElement newTarget = InputElement.GetContainingUIElement(rawStylusInputReport.StylusDevice.DirectlyOver as DependencyObject) as UIElement;
if (newTarget != null)
{
targetPIC = rawStylusInputReport.PenContext.Contexts.FindPlugInCollection(newTarget);
}
bool sendRawStylusInput = false;
if (targetPIC != null && rawStylusInputReport.RawStylusInput == null)
{
... // 忽略其他代碼
sendRawStylusInput = true;
}
... // 忽略其他代碼
}
通過以上代碼清晰可見,只有在 newTarget 能夠獲取 StylusPlugInCollection 對象,且 rawStylusInputReport.RawStylusInput 為空時才會設置 sendRawStylusInput 變量為 true 值
如果一個 UIElement 已經附加了 StylusPlugIn 了,自然就能讓 targetPIC 變量非空。那什么時候會出現 rawStylusInputReport.RawStylusInput == null 的情況?那就是如觸摸線程因為布局時機問題,導致無法拿到 StylusPlugIn 進而導致此是空。這預計就是 WPF 兜底邏輯最核心解決的問題了。但本文提到的是另一個情況,那就是觸摸線程卡頓,導致沒有執行。沒有執行導致 rawStylusInputReport.RawStylusInput 為空。于是此時 UI 線程代為觸發
本文代碼放在 github 和 gitee 上,可以使用如下命令行拉取代碼。我整個代碼倉庫比較龐大,使用以下命令行可以進行部分拉取,拉取速度比較快
先創建一個空文件夾,接著使用命令行 cd 命令進入此空文件夾,在命令行里面輸入以下代碼,即可獲取到本文的代碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 107caecee7f847355d23744bf6fc7b970d5e8c69
以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令行繼續輸入以下代碼,將 gitee 源換成 github 源進行拉取代碼。如果依然拉取不到代碼,可以發郵件向我要代碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 107caecee7f847355d23744bf6fc7b970d5e8c69
獲取代碼之后,進入 WPFDemo/LoqairjaniferNudalcefinay 文件夾,即可獲取到源代碼
更多技術博客,請參閱 博客導航
博客園博客只做備份,博客發布就不再更新,如果想看最新博客,請訪問 https://blog.lindexi.com/
如圖片看不見,請在瀏覽器開啟不安全http內容兼容

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名[林德熙](http://www.rzrgm.cn/lindexi)(包含鏈接:http://www.rzrgm.cn/lindexi ),不得用于商業目的,基于本文修改后的作品務必以相同的許可發布。如有任何疑問,請與我[聯系](mailto:lindexi_gd@163.com)。

浙公網安備 33010602011771號