在c#(ServiceBroker队列资源)中创建异步资源观察程序

在c#(ServiceBroker队列资源)中创建异步资源观察程序,c#,asynchronous,sqldatareader,cancellationtokensource,cancellation-token,C#,Asynchronous,Sqldatareader,Cancellationtokensource,Cancellation Token,作为探索异步的一部分练习,我想我应该尝试创建一个ServiceBrokerWatcher类。这个想法与文件系统观察者-观察资源并在发生事件时引发事件非常相似。我希望使用async来实现这一点,而不是实际创建线程,因为beast的本质意味着它大部分时间只是在等待SQLwaitfor(receive…语句。这似乎是异步的理想用法 我已经编写了“有效”的代码,当我通过代理发送消息时,类会注意到它并触发相应的事件。我觉得这太好了 但我怀疑在我对所发生的事情的理解中,我犯了一些根本性的错误,因为当我试图阻

作为探索异步的一部分练习,我想我应该尝试创建一个
ServiceBrokerWatcher
类。这个想法与
文件系统观察者
-观察资源并在发生事件时引发事件非常相似。我希望使用async来实现这一点,而不是实际创建线程,因为beast的本质意味着它大部分时间只是在等待SQL
waitfor(receive…
语句。这似乎是异步的理想用法

我已经编写了“有效”的代码,当我通过代理发送消息时,类会注意到它并触发相应的事件。我觉得这太好了

但我怀疑在我对所发生的事情的理解中,我犯了一些根本性的错误,因为当我试图阻止观察者时,它的行为并不像我预期的那样

首先简要概述组件,然后是实际代码:

我有一个存储过程,它发出一个
waitfor(receive…
,并在收到消息时将结果集返回给客户端

有一个
字典
,它将消息类型名称(在结果集中)映射到相应的事件处理程序。为了简单起见,我在示例中只有一种消息类型

watcher类有一个异步方法,它“永远”循环(直到请求取消),其中包含过程的执行和事件的引发

那么,有什么问题?我尝试在一个简单的winforms应用程序中托管我的类,当我点击按钮调用
StopListening()
方法(见下文)时,执行并没有像我想象的那样立即取消。行
listener?.Wait(10000)
实际上将等待10秒(或我设置的超时时间)。如果我观察SQL profiler发生了什么,我可以看到注意事件被“立即”发送,但函数仍然没有退出

我在代码中添加了以“!”开头的注释,我怀疑我误解了什么

那么,主要问题是:为什么我的
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);
                }
            }
        }
    }
}
//类成员//////////////////////
专用只读SqlConnection SqlConnection;
私有取消令牌源cts;
私有只读取消令牌ct;
私有任务侦听器;
私有只读字典映射;
公营机构
{
if(侦听器==null)
{
cts=新的CancellationTokenSource();
ct=cts.Token;
//!我怀疑将方法的结果分配给任务是错误的。。。
侦听器=ListenAsync(ct);
}
}
公营部门
{
尝试
{
cts.Cancel();
侦听器?等待(10000);/!出于某种原因等待整个10秒
}捕获(例外){
//陷阱sql在取消执行时将引发的异常
}最后
{
listener=null;
}
}
专用异步任务ListenAsync(CancellationToken ct)
{
使用(SqlCommand cmd=newsqlcommand(“events.dequeue_target”,sqlConnection))
使用(CancellationTokenRegistration ctr=ct.Register(cmd.Cancel))/!是否需要?
{
cmd.CommandTimeout=0;
而(!ct.iscancellationrequest)
{
var events=新列表();
使用(var rdr=await cmd.ExecuteReaderAsync(ct))
{
while(rdr.Read())
{
Add(rdr.GetString(rdr.GetOrdinal(“消息类型名称”));
}
}
foreach(events.Join(map,e=>e,m=>m.Key,(e,m=>m.Value))中的var处理程序)
{
if(handler!=null&!ct.IsCancellationRequested)
{
handler(this,null);
}
}
}
}
}

您不会显示如何将其绑定到WinForms应用程序,但如果您使用常规的
void按钮1单击
方法,则可能会遇到

因此,您的代码在控制台应用程序中运行良好(我尝试时会运行),但通过UI线程调用时会死锁

我建议将控制器类更改为公开
async
start和stop方法,并通过以下方式调用它们:

    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)
调用
executeaderasync(ct)
!啊哈!现在,类方法不必再是异步的,因为在
StopListening()
中,我可以只
listener.Wait(10000)
,等待将在不同的线程内部继续该任务,而使用者也不知道这一点。哦,孩子,真聪明

但是不,我不能那样做。至少在webforms应用程序中没有。如果我这样做,则文本框不会更新。原因似乎很清楚:ListenAsync的核心是调用一个事件处理程序,而该事件处理程序是一个想要更新文本框中文本的函数,这无疑必须发生在UI线程上。因此它不会死锁,但也无法更新UI。如果我在想要更新UI的处理程序中设置了一个断点,则会命中代码行,但UI无法更改

因此,最终看来,在这种情况下,唯一的解决方案确实是“一路异步”。或者在这种情况下,起来

我希望我没有