[英]Creating an async resource watcher in c# (service broker queue resource)
盡管我嘗試嘗試創建ServiceBrokerWatcher
類,但部分作為探索異步的練習。 這個想法與FileSystemWatcher
幾乎相同-監視資源並在發生某些情況時引發事件。 我希望通過異步而不是實際創建線程來執行此操作,因為野獸的本質意味着大多數情況下它只是在等待SQL waitfor (receive ...)
語句。 這似乎是異步的理想用法。
我編寫的代碼“有效”,因為當我通過代理發送消息時,類會注意到它並觸發適當的事件。 我以為這很整潔。
但是我懷疑在我對正在發生的事情的理解中某個地方發生了根本性的錯誤,因為當我嘗試停止觀察程序時,它的運行不符合我的預期。
首先是組件的簡要概述,然后是實際代碼:
我有一個存儲過程,該過程發出waitfor (receive...)
並在收到消息時將結果集返回給客戶端。
有一個Dictionary<string, EventHandler>
,它將消息類型名稱(在結果集中)映射到適當的事件處理程序。 為簡單起見,在示例中,我僅使用一種消息類型。
watcher類具有一個異步方法,該方法循環“永遠”(直到請求取消),該方法包含過程的執行和事件的引發。
所以有什么問題? 好吧,我嘗試將類托管在一個簡單的winforms應用程序中,當我按下按鈕調用StopListening()
方法(見下文)時,執行並沒有像我想的那樣立即被取消。 實際上,行listener?.Wait(10000)
將等待10秒(或者我設置超時的時間長)。 如果我看一下SQL事件探查器發生了什么,我可以看到注意力事件是“立即”發送的,但是該函數仍然沒有退出。
我已經在代碼中以“!”開頭添加了注釋。 我懷疑我誤會了什么。
所以,主要問題是:為什么我的ListenAsync
方法不“ ListenAsync
”我的取消請求?
另外,我是否認為該程序(大部分時間)僅消耗一個線程是正確的? 我做了危險的事嗎?
代碼如下,我盡力減少了它:
// class members //////////////////////
private readonly SqlConnection sqlConnection;
private CancellationTokenSource cts;
private readonly CancellationToken ct;
private Task listener;
private readonly Dictionary<string, EventHandler> map;
public void StartListening()
{
if (listener == null)
{
cts = new CancellationTokenSource();
ct = cts.Token;
// !I suspect assigning the result of the method to a Task is wrong somehow...
listener = ListenAsync(ct);
}
}
public void StopListening()
{
try
{
cts.Cancel();
listener?.Wait(10000); // !waits the whole 10 seconds for some reason
} catch (Exception) {
// trap the exception sql will raise when execution is cancelled
} finally
{
listener = null;
}
}
private async Task ListenAsync(CancellationToken ct)
{
using (SqlCommand cmd = new SqlCommand("events.dequeue_target", sqlConnection))
using (CancellationTokenRegistration ctr = ct.Register(cmd.Cancel)) // !necessary?
{
cmd.CommandTimeout = 0;
while (!ct.IsCancellationRequested)
{
var events = new List<string>();
using (var rdr = await cmd.ExecuteReaderAsync(ct))
{
while (rdr.Read())
{
events.Add(rdr.GetString(rdr.GetOrdinal("message_type_name")));
}
}
foreach (var handler in events.Join(map, e => e, m => m.Key, (e, m) => m.Value))
{
if (handler != null && !ct.IsCancellationRequested)
{
handler(this, null);
}
}
}
}
}
您沒有顯示如何將其綁定到WinForms應用程序,但是如果您使用常規的void button1click
方法,則可能會遇到此問題 。
因此,您的代碼將在控制台應用程序中正常運行(我嘗試時會運行),但通過UI線程調用時會死鎖。
我建議更改您的控制器類以公開async
啟動和停止方法,並通過例如調用它們:
private async void btStart_Click(object sender, EventArgs e)
{
await controller.StartListeningAsync();
}
private async void btStop_Click(object sender, EventArgs e)
{
await controller.StopListeningAsync();
}
彼得有正確的答案。 我對僵局感到困惑了幾分鍾,但隨后我的額頭拍了一下。 取消ExecuteReaderAsync之后,這是ListenAsync的繼續,因為這只是一個任務,而不是它自己的線程。 畢竟,這就是重點!
然后我想知道...好吧,如果我告訴ListenAsync()
的異步部分它不需要UI線程ListenAsync()
? 我將使用.ConfigureAwait(false)
調用ExecuteReaderAsync(ct)
.ConfigureAwait(false)
! 啊哈! 現在,類方法不必再異步了,因為在StopListening()
我可以只是listener.Wait(10000)
,等待將在另一個線程上在內部繼續執行任務,而使用方StopListening()
了。 哦,男孩,真聰明。
但是不,我不能那樣做。 至少不在Webforms應用程序中。 如果我這樣做,則文本框不會更新。 這樣做的原因似乎很清楚:ListenAsync的勇氣調用了一個事件處理程序,而該事件處理程序是一個想要更新文本框中文本的函數-毫無疑問,這必須發生在UI線程上。 因此,它不會死鎖,但也無法更新UI。 如果我在要更新UI的處理程序中設置了一個斷點,則會命中代碼行,但是UI不能更改。
因此,最終看來,在這種情況下,唯一的解決方案確實是“一直向下同步”。 或者在這種情況下,向上!
我希望我不必這樣做。 在我看來,Watcher的內部結構使用異步方法而不是僅生成線程,這是調用者不必關心的“實現細節”。 但是FileSystemWatcher具有完全相同的問題(需要control.Invoke
如果要基於監視程序事件更新GUI,請調用),所以還不錯。 如果我是必須在使用異步還是使用Invoke之間進行選擇的消費者,那么我會選擇異步!
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.