Частично, как упражнение по изучению асинхронности, я пытался создать класс 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);
}
}
}
}
}