Создание наблюдателя асинхронных ресурсов в 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!