【问题标题】:Creating an async resource watcher in c# (service broker queue resource)在 C# 中创建异步资源观察器(服务代理队列资源)
【发布时间】:2018-10-26 07:06:23
【问题描述】:

部分作为探索异步的练习,我想尝试创建一个ServiceBrokerWatcher 类。这个想法与FileSystemWatcher 非常相似 - 监视资源并在发生某些事情时引发事件。我希望通过异步而不是实际创建线程来做到这一点,因为野兽的本质意味着大多数时候它只是在等待 SQL waitfor (receive ...) 语句。这似乎是异步的理想用法。

我编写了“有效”的代码,因为当我通过代理发送消息时,类会注意到它并触发相应的事件。我认为这非常整洁。

但我怀疑我对正在发生的事情的理解存在根本性的错误,因为当我试图阻止观察者时,它的行为并不符合我的预期。

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

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

有一个Dictionary<string, EventHandler> 将消息类型名称(在结果集中)映射到适当的事件处理程序。为简单起见,我在示例中只有一种消息类型。

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

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

我已将 cmets 添加到以“!”开头的代码中我怀疑我误解了什么。

所以,主要问题:为什么我的 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);
                }
            }
        }
    }
}

【问题讨论】:

  • 附录:如果我启动查询探查器,我确实会在我调用 StopListening() 时看到注意事件几乎立即命中 SQL Server,因此该部分似乎正在工作,但 listenerTask 仍然没有完成。我觉得我可能在这里错过了一些非常明显的东西......
  • 调用ct.ThrowIfCancellationRequested() 而不是在ListenAsync 中检查ct.IsCancellationRequested
  • 谢谢科林,但我一定是做错了什么。我尝试在函数的几个不同位置(在 while 循环的开头和结尾)添加 ct.ThrowIfCancellationRequested(),但行为是相同的。

标签: c# asynchronous sqldatareader cancellationtokensource cancellation-token


【解决方案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();
    }

【讨论】:

  • 谢谢彼得。是的,我使用的是简单的 void 按钮处理程序,当您只需双击设计界面中的控件时就会自动创建这些处理程序。点击开始按钮时,应用程序已经响应。我可以使该调用异步,但它的行为方式相同 - 队列中的事件触发一个处理程序,该处理程序将一行文本添加到文本框。该程序仍然可以拖动等等。不过,我不确定我会在 StopListening() 方法中使用 await 什么。那里没有 awaitables,只有 cts.Cancel(),然后我给任务一个退出的机会。
  • 在进一步探索中,似乎只有 stop 方法需要异步。该程序在原始StartListening() 定义下仍能正常运行。另外,在StopListening() 方法中,我不需要添加.ConfigureAwait(false)。我可以做cts.Cancel(); await listener;。但我仍然不太明白是什么原因导致原始定义中的 10 秒等待。哪两个进程在死锁中争夺什么资源?如果其中之一是 ListenAsync() 方法本身,为什么在调用 StartListening() 时不会导致死锁?
  • AFAIK 死锁是因为 UI 线程阻塞等待任务完成,任务需要 async 方法在完成之前运行延续,并且任务已经捕获了 UI 线程上下文安排完成时间。它不能因为 UI 线程被阻塞。
  • StartListening() 不受影响,因为它实际上并没有等待任何东西,所以是的,它可能只是一个常规的 void 方法。或者它可以返回 Task.CompletedTask 并且可以等待而不是实际上是 async,只是为了对称。
【解决方案2】:

彼得有正确的答案。我对什么是僵局感到困惑几分钟,但随后我的额头被拍了一下。它是 ExecuteReaderAsync 取消后 ListenAsync 的延续,因为它只是一个任务,而不是它自己的线程。毕竟,这才是重点!

然后我想知道...好吧,如果我告诉 ListenAsync() 的异步部分它不需要 UI 线程怎么办。我会打电话给ExecuteReaderAsync(ct).ConfigureAwait(false)!啊哈!现在类方法不必再异步了,因为在StopListening() 我可以只使用listener.Wait(10000),等待将在不同的线程内部继续执行任务,而消费者并不聪明。哦,男孩,太聪明了。

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

所以最后看来,在这种情况下唯一的解决方案确实是“一直异步”。或者在这种情况下,向上!

我希望我不必这样做。在我看来,我的 Watcher 的内部使用异步方法而不是仅仅产生线程这一事实是调用者不必关心的“实现细节”。但是 FileSystemWatcher 具有完全相同的问题(如果您想根据观察者事件更新 GUI,则需要 control.Invoke),所以这还不错。如果我是一个必须在使用异步或使用 Invoke 之间进行选择的消费者,我会选择异步!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-06-12
    • 1970-01-01
    • 2014-05-29
    • 2011-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-23
    相关资源
    最近更新 更多