在C#(服务代理队列资源)中创建异步资源观察程序

时间:2018-10-26 07:06:24

标签: c# asynchronous sqldatareader cancellationtokensource cancellation-token

作为探索异步的一种练习,尽管我会尝试创建一个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);
                }
            }
        }
    }
}

2 个答案:

答案 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之间进行选择的消费者,那么我会选择异步!