我有一項服務可以針對第 3 方軟體(通過 .NET UIAutomation)執行一些復雜的自動化。該服務由一個主機組成,該主機啟動另一個 STA 執行緒以在主機通過ManualResetEvent. 在決定在面向用戶的環境中使用它之前,這一直很有效。那時,我創建了一個AutomationMessenger類來驅動 UI/WPF 應用程式的狀態更新。這運作良好。然后在某個時候,我需要在自動化程序中獲取并驗證用戶輸入并將其驅動回自動化執行緒——這是車輪從公共汽車上掉下來的地方。我向AutomationMessenger,一切似乎都很好。我最終改變了一些事情,并且在自動化的不同點需要用戶輸入。最終,自動化類中的方法開始被呼叫兩次——所以我知道我遇到了一些執行緒問題。
毫不奇怪,這些問題在測驗中不存在,或者在服務運行時沒有引發用戶通知。
執行緒按預期創建,并將訊息從自動化發送到 UI 層以由Application.Current.Dispatcher作業呼叫以更新 UI,但是當在 UI 執行緒上呼叫自動化代碼/執行緒內的回呼操作時,控制永遠不會回傳給那個執行緒。我可以在 VS 的“執行緒”視窗中看到它發生。
這對我來說并不奇怪,我只是不知道如何解決它。
我花了一些精力將這段代碼的復雜性歸結為一個最小的、可重現的示例,但我已經做到了,它說明了這個問題。
GitHub-AutomationThreadingIssue
我花了相當多的時間閱讀關于SynchronizationContexts 的內容,并且我認為它就像將背景關系從自動化執行緒傳遞到 UI 一樣簡單Send,Post但是了解到作業執行緒不會自動獲取背景關系.
簡單地在自動化執行緒上呼叫 Join() 似乎會殺死自動化執行緒,或者將其合并到 UI 執行緒中。
Task.Run(() => Callback)從UI 執行緒開始一個新的任務(第三方軟體(通過Automation.AddAutomationEventHandler)。
在搜索了“跨執行緒訊息傳遞”的所有可能性之后,共識是BlockingCollection/ ConcurrentQueue,或者Channels。該專案僅限于 net45,排除了頻道(最低要求為 4.6)。如果不阻塞自動化執行緒中的執行,我就無法讓阻塞集合實作作業。
我看到了很多從作業執行緒在 UI 執行緒上呼叫委托/操作的解決方案,但對于從作業執行緒上的 UI 執行緒呼叫方法沒有任何用處。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Thread.CurrentThread.Name = "WPF Thread";
AutomationMessenger.Received = AutomationMessenger_Received;
}
private void AutomationMessenger_Received(object sender, AutomationMessengerEventArgs e)
{
switch (e.Name)
{
case "Status":
Application.Current.Dispatcher.Invoke(() =>
{
Status_TextBlock.Text = e.Message;
});
break;
case "UserInput":
Application.Current.Dispatcher.Invoke(() =>
{
AutomationInput_Grid.IsEnabled = true;
Input_Button.Tag = e.CallbackAction;
Status_TextBlock.Text = e.Message;
});
break;
}
}
private void Run_Button_Click(object sender, RoutedEventArgs e)
{
Run_Button.IsEnabled = false;
var backgroundAutomationTask = new Task(() =>
{
var worker = new AutomationBackgroundWorker();
worker.Begin();
});
backgroundAutomationTask.Start();
}
private void Input_Button_Click(object sender, RoutedEventArgs e)
{
if (Input_Button.Tag != null && Input_Button.Tag is Action<string> callbackAction)
{
callbackAction.Invoke(Input_TextBox.Text);
}
}
}
public class AutomationBackgroundWorker
{
private ManualResetEvent manualResetEvent;
private Thread automationThread;
private SynchronizationContext backgroundWorkerContext;
public AutomationBackgroundWorker()
{
Thread.CurrentThread.Name = "Automation Background Worker";
backgroundWorkerContext = SynchronizationContext.Current;
automationThread = new Thread(this.Automation);
automationThread.Name = "Automation Thread";
automationThread.SetApartmentState(ApartmentState.STA);
manualResetEvent = new ManualResetEvent(false);
}
public void Begin()
{
automationThread.Start();
manualResetEvent.WaitOne();
}
public void Complete()
{
manualResetEvent.Set();
manualResetEvent.Close();
}
public void Automation()
{
var automationSyncContext = SynchronizationContext.Current;
AutomationMessenger.Send("Status", "Initializing", null);
// Initialize Automation Elements, etc..
Task.Delay(1000).Wait();
AutomationMessenger.Send("Status", "Initialized", null);
// Get user input
AutomationMessenger.Send("UserInput", "Please Enter Your Name", UserInputCallback);
// Wait for their response
}
// As you would expect, this method is being called from the WPF Thread / UI Thread
// How can I synchronize it back to the Automation Thread
private void UserInputCallback(string userInput)
{
//if (Thread.CurrentThread != automationThread)
//{
// automationThread.Join();
//}
// Doing this blocks and kills the Automation Thread
if (string.IsNullOrWhiteSpace(userInput)) return;
AutomationMessenger.Send("Status", $"Thanks {userInput}", null);
Complete();
}
}
public static class AutomationMessenger
{
public static event AutomationMessengerEventHandler Received;
public static void Send(string name, string message, Action<string> callbackAction) => Received?.Invoke(null, new AutomationMessengerEventArgs(name, message, callbackAction));
}
public delegate void AutomationMessengerEventHandler(object sender, AutomationMessengerEventArgs e);
public class AutomationMessengerEventArgs : EventArgs
{
public string Name { get; set; }
public string Message { get; set; }
public Action<string> CallbackAction { get; set; }
public AutomationMessengerEventArgs(string name, string message, Action<string> callbackAction) : base()
{
Name = name;
Message = message;
CallbackAction = callbackAction;
}
}
uj5u.com熱心網友回復:
在花了一天的大部分時間嘗試實作一個 customSynchronizationContext并在這方面取得了相當大的成功之后,我偶然發現了一種根據BlockingCollection<T>我的需要制作作品的方法。
TL; DR 對于建議使用 aBlockingCollection<T>來確保Actions/delegates/methods 在一個執行緒上運行的帖子,這是一個歸結為(您需要添加自己的錯誤處理)示例:
PoorMansSynchronizier又名AutomataionActionQueue。
public class PoorMansSynchronizier
{
private readonly BlockingCollection<Action> _queue;
private readonly Thread _thread;
public PoorMansSynchronizier(Action<Thread> options)
{
_queue = new BlockingCollection<Action>();
_thread = new Thread(() => Execute());
options?.Invoke(_thread);
if (!_thread.IsAlive) _thread.Start();
}
public void Invoke(Action action) => _queue.Add(action);
public void Complete() => _queue.CompleteAdding();
private void Execute()
{
foreach (var action in _queue.GetConsumingEnumerable())
{
action.Invoke();
}
}
}
更新了后臺作業者服務
public class AutomationBackgroundWorker
{
private ManualResetEvent manualResetEvent;
private PoorMansSynchronizier poorMansSynchronizier;
public AutomationBackgroundWorker()
{
Thread.CurrentThread.Name = "Automation Background Worker";
poorMansSynchronizier = new PoorMansSynchronizier(thread =>
{
thread.Name = "Automation Thread";
thread.SetApartmentState(ApartmentState.STA);
});
manualResetEvent = new ManualResetEvent(false);
}
public void Begin()
{
poorMansSynchronizier.Invoke(() => Automation());
manualResetEvent.WaitOne();
}
public void Complete()
{
manualResetEvent.Set();
manualResetEvent.Close();
poorMansSynchronizier.Complete();
}
public void Automation()
{
AutomationMessenger.Send("Status", "Initializing", null);
// Initialize Automation Elements, etc..
Task.Delay(1000).Wait();
AutomationMessenger.Send("Status", "Initialized", null);
// Get user input
AutomationMessenger.Send("UserInput", "Please Enter Your Name", UserInputCallback);
// Wait for their response
}
private void UserInputCallback(string userInput)
{
poorMansSynchronizier.Invoke(() =>
{
// Do something with the response
if (string.IsNullOrWhiteSpace(userInput)) return;
AutomationMessenger.Send("Status", $"Thanks {userInput}", null);
Complete();
});
}
}
這里的關鍵是,窮人的同步器有自己的Thread,它的目的是執行佇列中的每一項。
我能夠取消后臺作業人員必須automationThread在原始問題代碼中保留對舊的參考,現在我需要在 STA 自動化執行緒上運行的任何操作,我只需傳遞到佇列。
這對非面向用戶的自動化代碼的原始實作沒有影響——據它所知,它仍在一個名為 Automation Thread 的 STA 執行緒上運行,并且與實作面向用戶的需求之前一樣快樂- 沒有厄運和悲觀或嚴重的技術債務,只有自動化代碼的巨大重構才能解決@Enigmativity。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/417325.html
標籤:
上一篇:C#中跨越后臺執行緒的等待運算子
