Создание наблюдателя асинхронных ресурсов в c # (ресурс очереди компонента Service Broker) - PullRequest
0 голосов
/ 26 октября 2018

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

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

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

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

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

Существует 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 ]

0 голосов
/ 26 октября 2018

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

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

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

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

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

0 голосов
/ 26 октября 2018

Вы не показываете, как вы связали его с приложением 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();
    }
...