Создание наблюдателя асинхронных ресурсов в C# (ресурс очереди компонента Service Broker)

Частично как упражнение в изучении асинхронности, я пытался создать ServiceBrokerWatcher учебный класс. Идея очень похожа на FileSystemWatcher - смотреть ресурс и поднять событие, когда что-то происходит. Я надеялся сделать это с помощью асинхронного, а не фактического создания потока, потому что природа зверя означает, что большую часть времени он просто ожидает на SQL waitfor (receive ...) заявление. Это казалось идеальным использованием асинхронного.

Я написал код, который "работает", когда я отправляю сообщение через посредника, класс замечает его и запускает соответствующее событие. Я думал, что это было супер аккуратно.

Но я подозреваю, что где-то в моем понимании происходящего произошло что-то принципиально неправильное, потому что, когда я пытаюсь остановить наблюдателя, оно не ведет себя так, как я ожидаю.

Сначала краткий обзор компонентов, а затем фактический код:

У меня есть хранимая процедура, которая выдает waitfor (receive...) и возвращает набор результатов клиенту при получении сообщения.

E сть Dictionary<string, EventHandler> который сопоставляет имена типов сообщений (в наборе результатов) с соответствующим обработчиком событий. Для простоты у меня только один тип сообщения в примере.

Класс наблюдателя имеет асинхронный метод, который зацикливается "навсегда" (до тех пор, пока не будет запрошено аннулирование), который содержит выполнение процедуры и получение событий.

Так в чем проблема? Ну, я попытался разместить свой класс в простом приложении 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 ответа

Решение

Вы не показываете, как вы связали его с приложением WinForms, но если вы используете обычные void button1click методы, вы можете столкнуться с этой проблемой.

Таким образом, ваш код будет нормально работать в консольном приложении (так и будет, когда я его попробую), но будет зависать при вызове через поток пользовательского интерфейса.

Я бы предложил изменить ваш класс контроллера, чтобы выставить 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();
    }

У Питера был правильный ответ. В течение нескольких минут я не мог понять, что же зашло в тупик, но потом я ударил себя по лбу. Это продолжение ListenAsync после отмены ExecuteReaderAsync, потому что это просто задача, а не отдельный поток. Это был, в конце концов, весь смысл!

Тогда я подумал... Хорошо, что если я расскажу асинхронную часть ListenAsync() что ему не нужен поток пользовательского интерфейса. Я позвоню ExecuteReaderAsync(ct) с .ConfigureAwait(false)! Ага! Теперь методы класса больше не должны быть асинхронными, потому что в StopListening() Я могу просто listener.Wait(10000)ожидание продолжит задачу внутри другого потока, и потребитель не станет мудрее. О, мальчик, такой умный.

Но нет, я не могу этого сделать. По крайней мере, не в приложении webforms. Если я это сделаю, то текстовое поле не будет обновлено. И причина этого кажется достаточно ясной: внутренности ListenAsync вызывают обработчик событий, и этот обработчик событий является функцией, которая хочет обновить текст в текстовом поле - что, без сомнения, должно происходить в потоке пользовательского интерфейса. Таким образом, он не блокируется, но также не может обновлять интерфейс. Если я устанавливаю точку останова в обработчике, который хочет обновить пользовательский интерфейс, строка кода будет нажата, но пользовательский интерфейс не может быть изменен.

Таким образом, в конце концов, кажется, что единственное решение в этом случае - это "идти асинхронно до конца". Или в этом случае up!

Я надеялся, что мне не нужно было этого делать. Тот факт, что внутреннее устройство моего Watcher использует асинхронные методологии, а не просто порождает поток, на мой взгляд, является "деталью реализации", о которой вызывающий не должен заботиться. Но FileSystemWatcher имеет точно такую ​​же проблему (необходимость control.Invoke если вы хотите обновить графический интерфейс на основе события наблюдателя), это не так уж плохо. Если бы я был потребителем, который должен был выбирать между использованием async или Invoke, я бы выбрал async!

Другие вопросы по тегам