作为探索异步的一种练习,尽管我会尝试创建一个ServiceBrokerWatcher
类。这个想法与FileSystemWatcher
大致相同-观看资源并在发生某些情况时引发事件。我希望通过异步而不是实际创建线程来执行此操作,因为野兽的本质意味着大多数时候它只是在等待SQL waitfor (receive ...)
语句。这似乎是异步的理想用法。
我编写了“有效”的代码,因为当我通过代理发送消息时,类会注意到它并触发适当的事件。我以为这很整洁。
但是我怀疑我在了解发生了什么事情时在某处出现了根本性的错误,因为当我尝试停止观察程序时,它的运行不符合我的预期。
首先简要介绍各个组件,然后再进行实际代码:
我有一个存储过程,该过程发出waitfor (receive...)
并在收到消息时将结果集返回给客户端。
有一个Dictionary<string, EventHandler>
,它将消息类型名称(在结果集中)映射到适当的事件处理程序。为简单起见,在示例中,我仅使用一种消息类型。
watcher类具有一个异步方法,该方法循环“永远”(直到请求取消),该方法包含过程的执行和事件的引发。
那是什么问题?好吧,我尝试将类托管在一个简单的winforms应用程序中,当我按下按钮调用StopListening()
方法(见下文)时,执行并没有立即取消,就像我认为的那样。实际上,listener?.Wait(10000)
行将等待10秒(或者我设置超时的时间长)。如果我看一下SQL事件探查器发生了什么,我可以看到注意力事件是“立即”发送的,但是该函数仍然没有退出。
我已在代码中以“!”开头添加了注释。我怀疑我误解了什么。
因此,主要问题是:为什么我的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);
}
}
}
}
}
答案 0 :(得分:1)
您没有显示如何将其绑定到WinForms应用程序,但是如果您使用常规的void button1click
方法,则可能会遇到this issue。
因此,您的代码将在控制台应用程序中正常运行(我尝试时会运行),但是在通过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();
}
答案 1 :(得分:1)
彼得的回答正确。我对僵局感到困惑了几分钟,但随后我的额头拍了一下。取消ExecuteReaderAsync之后,这是ListenAsync的继续,因为这只是一个任务,而不是它自己的线程。毕竟,这就是重点!
然后我想知道...好吧,如果我告诉ListenAsync()
的异步部分不需要UI线程怎么办?我将用ExecuteReaderAsync(ct)
呼叫.ConfigureAwait(false)
!啊哈!现在,类方法不必再异步了,因为在StopListening()
中我可以listener.Wait(10000)
,等待将在另一个线程上在内部继续执行任务,而使用方则不是明智的选择。哦,男孩,真聪明。
但是不,我不能那样做。至少不在Webforms应用程序中。如果我这样做,则文本框不会更新。这样做的原因似乎很清楚:ListenAsync的勇气调用了一个事件处理程序,而该事件处理程序是一个想要更新文本框中文本的函数-毫无疑问,这必须发生在UI线程上。因此,它不会死锁,但也无法更新UI。如果我在要更新UI的处理程序中设置了一个断点,则会命中代码行,但是UI不能更改。
因此,最终看来,在这种情况下,唯一的解决方案确实是“一直向下同步”。或者在这种情况下,向上!
我希望我不必这样做。在我看来,Watcher的内部结构使用异步方法而不是仅生成线程,这是调用者不必关心的“实现细节”。但是FileSystemWatcher具有完全相同的问题(如果要基于监视程序事件更新GUI,则需要control.Invoke
),所以还不错。如果我是必须在使用异步还是使用Invoke之间进行选择的消费者,那么我会选择异步!