在 C# 中创建异步资源观察器(服务代理队列资源)
Creating an async resource watcher in c# (service broker queue resource)
部分作为探索异步的练习,我想尝试创建一个 ServiceBrokerWatcher
class。这个想法与 FileSystemWatcher
非常相似 - 观察资源并在发生某些事情时引发事件。我希望用异步而不是实际创建一个线程来做到这一点,因为野兽的本性意味着大部分时间它只是在等待 SQL waitfor (receive ...)
语句。这似乎是异步的理想用法。
我编写了 "works" 代码,因为当我通过代理发送消息时,class 会注意到它并触发相应的事件。我认为这非常整洁。
但我怀疑我对正在发生的事情的理解有些根本性的错误,因为当我试图停止观察者时,它的行为并不像我预期的那样。
首先简单介绍一下组件,然后是实际代码:
我有一个存储过程,它在收到消息时向客户端发出 waitfor (receive...)
和 returns 结果集。
有一个 Dictionary<string, EventHandler>
将消息类型名称(在结果集中)映射到适当的事件处理程序。为简单起见,我在示例中只有一种消息类型。
观察者 class 有一个循环 "forever" 的异步方法(直到请求取消),其中包含过程的执行和事件的引发。
所以,问题是什么?好吧,我尝试在一个简单的 winforms 应用程序中托管我的 class,当我按下按钮调用 StopListening()
方法(见下文)时,执行并没有像我想象的那样立即取消. listener?.Wait(10000)
行实际上会等待 10 秒(或者我设置的超时时间)。如果我观察 SQL 探查器发生了什么,我可以看到正在发送注意事件 "straight away",但该函数仍然没有退出。
我已经为以“!”开头的代码添加了注释。我怀疑我误解了什么。
所以,主要问题:为什么我的 ListenAsync
方法 "honoring" 不是我的取消请求?
另外,我认为这个程序(大部分时间)只消耗一个线程是对的吗?我做过什么危险的事吗?
代码如下,我尽量减少它:
// 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
方法,则可能 运行 进入 this issue。
因此,您的代码在控制台应用程序中 运行 正常(我尝试时它确实如此),但在通过 UI 线程调用时会死锁。
我建议更改您的控制器 class 以公开 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 线程呢?我会用.ConfigureAwait(false)
叫ExecuteReaderAsync(ct)
!啊哈!现在 class 方法不必再异步了,因为在 StopListening()
中我可以 listener.Wait(10000)
,等待将在不同的线程内部继续任务,而消费者是 none越聪明。小伙子,真聪明
但是不,我不能那样做。至少在 webforms 应用程序中没有。如果我这样做,则文本框不会更新。其原因似乎很清楚:ListenAsync 的内部调用一个事件处理程序,而该事件处理程序是一个想要更新文本框中的文本的函数——这无疑必须在 UI 线程上发生。所以它不会死锁,但它也无法更新 UI。如果我在要更新 UI 的处理程序中设置断点,则会命中代码行,但无法更改 UI。
所以最后似乎在这种情况下唯一的解决办法确实是"go async all the way down"。或者在这种情况下,up!
我希望我不必那样做。事实上,我的 Watcher 的内部正在使用异步方法而不是仅仅生成一个线程,在我看来,这是调用者不必关心的 "implementation detail"。但是 FileSystemWatcher 有完全相同的问题(如果您想根据观察者事件更新 GUI,则需要 control.Invoke
),所以这还不错。如果我是必须在使用异步或使用 Invoke 之间做出选择的消费者,我会选择异步!
部分作为探索异步的练习,我想尝试创建一个 ServiceBrokerWatcher
class。这个想法与 FileSystemWatcher
非常相似 - 观察资源并在发生某些事情时引发事件。我希望用异步而不是实际创建一个线程来做到这一点,因为野兽的本性意味着大部分时间它只是在等待 SQL waitfor (receive ...)
语句。这似乎是异步的理想用法。
我编写了 "works" 代码,因为当我通过代理发送消息时,class 会注意到它并触发相应的事件。我认为这非常整洁。
但我怀疑我对正在发生的事情的理解有些根本性的错误,因为当我试图停止观察者时,它的行为并不像我预期的那样。
首先简单介绍一下组件,然后是实际代码:
我有一个存储过程,它在收到消息时向客户端发出 waitfor (receive...)
和 returns 结果集。
有一个 Dictionary<string, EventHandler>
将消息类型名称(在结果集中)映射到适当的事件处理程序。为简单起见,我在示例中只有一种消息类型。
观察者 class 有一个循环 "forever" 的异步方法(直到请求取消),其中包含过程的执行和事件的引发。
所以,问题是什么?好吧,我尝试在一个简单的 winforms 应用程序中托管我的 class,当我按下按钮调用 StopListening()
方法(见下文)时,执行并没有像我想象的那样立即取消. listener?.Wait(10000)
行实际上会等待 10 秒(或者我设置的超时时间)。如果我观察 SQL 探查器发生了什么,我可以看到正在发送注意事件 "straight away",但该函数仍然没有退出。
我已经为以“!”开头的代码添加了注释。我怀疑我误解了什么。
所以,主要问题:为什么我的 ListenAsync
方法 "honoring" 不是我的取消请求?
另外,我认为这个程序(大部分时间)只消耗一个线程是对的吗?我做过什么危险的事吗?
代码如下,我尽量减少它:
// 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
方法,则可能 运行 进入 this issue。
因此,您的代码在控制台应用程序中 运行 正常(我尝试时它确实如此),但在通过 UI 线程调用时会死锁。
我建议更改您的控制器 class 以公开 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 线程呢?我会用.ConfigureAwait(false)
叫ExecuteReaderAsync(ct)
!啊哈!现在 class 方法不必再异步了,因为在 StopListening()
中我可以 listener.Wait(10000)
,等待将在不同的线程内部继续任务,而消费者是 none越聪明。小伙子,真聪明
但是不,我不能那样做。至少在 webforms 应用程序中没有。如果我这样做,则文本框不会更新。其原因似乎很清楚:ListenAsync 的内部调用一个事件处理程序,而该事件处理程序是一个想要更新文本框中的文本的函数——这无疑必须在 UI 线程上发生。所以它不会死锁,但它也无法更新 UI。如果我在要更新 UI 的处理程序中设置断点,则会命中代码行,但无法更改 UI。
所以最后似乎在这种情况下唯一的解决办法确实是"go async all the way down"。或者在这种情况下,up!
我希望我不必那样做。事实上,我的 Watcher 的内部正在使用异步方法而不是仅仅生成一个线程,在我看来,这是调用者不必关心的 "implementation detail"。但是 FileSystemWatcher 有完全相同的问题(如果您想根据观察者事件更新 GUI,则需要 control.Invoke
),所以这还不错。如果我是必须在使用异步或使用 Invoke 之间做出选择的消费者,我会选择异步!