Изучаю .NET, возникают вопросы .
В частности, не совсем понятно как решается проблема отмены асинхронных вызовов.
К примеру, у меня есть слушающий сокет, которому я сказал BeginAccept.
В какой-то момент, я решаю остановить прослушивание и вызываю socket.Close().
Что произойдет, если как раз в этот момент исполняется код из accept callback-а?
Ведь он может обращаться к объектам которые уже disposed, верно?
Следущий код, демонстрирует проблему (на примере таймера):
using System;
using System.ComponentModel;
using System.Timers;
namespace test
{
class Test
{
private Timer timer;
private System.Threading.AutoResetEvent evt;
private void OnTimer(object sender, ElapsedEventArgs args)
{
evt.Set(); // <-- Упс! Object disposed.
}
public void RunTest()
{
using (evt = new System.Threading.AutoResetEvent(false))
{
using (timer = new Timer())
{
timer.Elapsed += this.OnTimer;
timer.Interval = 10;
timer.Enabled = true;
evt.WaitOne(9, false);
timer.Enabled = false;
}
}
}
public static void Run()
{
var test = new Test();
test.RunTest();
}
}
class Program
{
private static void Main(string[] args)
{
while (true)
{
Test.Run();
}
}
}
}
Собственно вопрос состоит в следующем: есть ли стандартное средство, позволяющее дождаться завершения исполняющихся асинхронных вызовов перед dispos-ом объекта? Либо нужно городить дополнительную синхронизацию самому (очень не хочется).
Здравствуйте, jedi, Вы писали:
J>Собственно вопрос состоит в следующем: есть ли стандартное средство, позволяющее дождаться завершения исполняющихся асинхронных вызовов перед dispos-ом объекта? Либо нужно городить дополнительную синхронизацию самому (очень не хочется).
J>Спасибо!
Есть такое средство
IAsyncResult ar = socket.BeginAccept();
ar.AsyncWaitHandle.WaitOne();
Здравствуйте, Аlexey, Вы писали:
А>Есть такое средство А>
А>IAsyncResult ar = socket.BeginAccept();
А>ar.AsyncWaitHandle.WaitOne();
А>
Чудесное средство, только не для того случая, который я описал.
Еще раз — я хочу остановить сервер и отменить прослушку. Насколько я понял,
официальный способ — это Close. Далее, проблемы описаны в первом посте.
Здравствуйте, jedi, Вы писали:
J>В частности, не совсем понятно как решается проблема отмены асинхронных вызовов. J>К примеру, у меня есть слушающий сокет, которому я сказал BeginAccept. J>В какой-то момент, я решаю остановить прослушивание и вызываю socket.Close(). J>Что произойдет, если как раз в этот момент исполняется код из accept callback-а? J>Ведь он может обращаться к объектам которые уже disposed, верно?
Если "остановка прослушивания" — штатная операция (а иначе и быть не может, ибо к остановке приводит явный вызов), то нет ничего страшного в том, что бы хранить специальный флаг, сообщающий о том, "остановлена ли прослушка".
Help will always be given at Hogwarts to those who ask for it.
Здравствуйте, _FRED_, Вы писали:
_FR>Здравствуйте, jedi, Вы писали:
J>>В частности, не совсем понятно как решается проблема отмены асинхронных вызовов. J>>К примеру, у меня есть слушающий сокет, которому я сказал BeginAccept. J>>В какой-то момент, я решаю остановить прослушивание и вызываю socket.Close(). J>>Что произойдет, если как раз в этот момент исполняется код из accept callback-а? J>>Ведь он может обращаться к объектам которые уже disposed, верно?
_FR>Если "остановка прослушивания" — штатная операция (а иначе и быть не может, ибо к остановке приводит явный вызов), то нет ничего страшного в том, что бы хранить специальный флаг, сообщающий о том, "остановлена ли прослушка".
В общем, Вы правы. Вот только флагом (булевской переменной) здесь не обойтись. По крайней мере, я не вижу как это сделать?
Если не сложно, не могли бы Вы показать как это сделать в примере с таймером?
И еще = я знаю про Timer.Dispose(WaitHandle) но, к сожалению в том же сокете аналогов нет,
поэтому нужно более общее решение.
Здравствуйте, jedi, Вы писали:
J>К примеру, у меня есть слушающий сокет, которому я сказал BeginAccept. J>В какой-то момент, я решаю остановить прослушивание и вызываю socket.Close(). J>Что произойдет, если как раз в этот момент исполняется код из accept callback-а? J>Ведь он может обращаться к объектам которые уже disposed, верно?
Disposed в этом случае может быть только один объект — оригинальный слушающий сокет. Ну получите исключение ObjectDisposedException при вызове EndAccept(). Пока проблем не вижу. Я что-то не понимаю ?
J>Следущий код, демонстрирует проблему (на примере таймера):
Э-э-э... Так Вам сокеты, или всё-таки что-то другое ?
Здравствуйте, drol, Вы писали:
D>Здравствуйте, jedi, Вы писали:
D>Disposed в этом случае может быть только один объект — оригинальный слушающий сокет.
Не совсем. Может быть что угодно, зависит от логики программы.
В частности, в примере, происходит обращение к убитому ивенту.
D>Ну получите исключение ObjectDisposedException при вызове EndAccept(). Пока проблем не вижу. Я что-то не понимаю ?
Я как-то не привык, чтоб у меня такие исключения просто так летали. Может тяжелое наследие С++
D>Э-э-э... Так Вам сокеты, или всё-таки что-то другое ?
Пример с таймером — для простоты. На самом деле проблема общая, не только сокетов касается ...
Здравствуйте, jedi, Вы писали:
D>>Disposed в этом случае может быть только один объект — оригинальный слушающий сокет. J>Не совсем. Может быть что угодно, зависит от логики программы. J>В частности, в примере, происходит обращение к убитому ивенту.
Ну так не надо делать такую логику, иначе зачем Вам асинхронное I/O сотоварищи ?
D>>Ну получите исключение ObjectDisposedException при вызове EndAccept(). Пока проблем не вижу. Я что-то не понимаю ? J>Я как-то не привык, чтоб у меня такие исключения просто так летали.
EndAccept() может бросать объекты минимум 6-ти классов исключений. Это не касаясь того, что код делегата работает в потоке из пула. В связи с чем в его теле не помешает ловить вообще всё что летит. Как минимум с целью логирования...
J>Может тяжелое наследие С++
Привыкайте
J>Пример с таймером — для простоты.
Как раз пример с таймером сложнее, на мой взгляд. Бо там может быть непонятно сколько одновременно исполняющихся вызовов делегата, и непонятно сколько ещё в очереди (и есть ли она вообще ???)
Тогда как для нормальных реализаций на BeginXXX()/EndXXX() срабатываний делегата в итоге будет столько, сколько прошло успешных вызовов BeginXXX()
J>На самом деле проблема общая, не только сокетов касается ...
В случае сокетов вызов Socket.Close() иницирует срабатывание всех "ждущих" делегатов успешных вызовов BeginXXX(). Так что достаточно, например, завести семафор/событие и по нему дожидаться окончания отработки делегатов.
Состояние закрытия, как уже выше предлагали, можно отслеживать хоть булевым флагом (конечно, с volatile). Ну а методы сокета в соответствующих случаях бросают исключения.
*Да, и не забывайте, делегаты Timer'а и BeginXXX() работают в потоках из пула, а они все background-потоки...
Здравствуйте, Аlexey, Вы писали:
J>>Собственно вопрос состоит в следующем: есть ли стандартное средство, позволяющее дождаться завершения исполняющихся асинхронных вызовов перед dispos-ом объекта? Либо нужно городить дополнительную синхронизацию самому (очень не хочется).
А>Есть такое средство А>
А>IAsyncResult ar = socket.BeginAccept();
А>ar.AsyncWaitHandle.WaitOne();
А>
Это средство не поможет. В случае сокета, WaitOne() ожидает (если не вдаваться в подробности) не окончание исполнения делегата, а момент отправки делегата на исполнение.
Здравствуйте, drol, Вы писали:
D>Здравствуйте, Аlexey, Вы писали:
D>Это средство не поможет. В случае сокета, WaitOne() ожидает (если не вдаваться в подробности) не окончание исполнения делегата, а момент отправки делегата на исполнение.
Можно ссылку на документацию где это сказано? (хочется разобраться в деталях)
Здравствуйте, drol, Вы писали:
D>*Да, и не забывайте, делегаты Timer'а и BeginXXX() работают в потоках из пула, а они все background-потоки...
Как все сложно... Кстати, не могли бы Вы ткнуть в какую-нибудь опенсорсную реализацию сервера на .NET, которая по-Вашему может служить хорошим примером для изучения этих вещей.
Здравствуйте, jedi, Вы писали:
D>>Это средство не поможет. В случае сокета, WaitOne() ожидает (если не вдаваться в подробности) не окончание исполнения делегата, а момент отправки делегата на исполнение. J>Можно ссылку на документацию где это сказано?
Читайте доку на IAsyncResult и его окрестности.
J>(хочется разобраться в деталях)
Да там вообще-то всё просто. AsyncWaitHandle возвращает объект для ожидания окончания асинхронного вызова. Асинхронный вызов в нашем случае это не вызов делегата. Это вызов какого-нибудь AcceptEx() для Win32'шного сокета где-то в дебрях BeginAccept(). И взведение AsyncWaitHandle означает лишь то, что теперь можно спокойно дёргать EndAccept(), и получать результат асинхронного вызова без блокировки. В нашем случае — новый сокет с установленным соединением.
Можно, например, вообще вот так писать:
var r = sock.BeginAccept(null, null);
var c = sock.EndAccept(r);
И всё будет работать, просто EndAccept() заблокируется до момента установления соединения.
Чтобы было удобней устраивать сложные внешние синхронизации (ну там через WaitForMultipleObjects() какие-нибудь), объект ожидания вытащили прямиком в IAsyncResult.AsyncWaitHandle:
var r = sock.BeginAccept(null, null);
r.AsyncWaitHandle.WaitOne();
var c = sock.EndAccept(r);
Работает аналогично первому примеру.
Ещё один способ ожидания: поллинг через IAsyncResult.IsCompleted Иногда требуется
Ну и, наконец, то что мы обсуждали с самого начала: делегат, вызывающийся в некотором потоке из пула, после окончания отработки исходного (настоящего) асинхронного вызова.
*Надеюсь стало яснее
Здравствуйте, jedi, Вы писали:
J>Как все сложно... Кстати, не могли бы Вы ткнуть в какую-нибудь опенсорсную реализацию сервера на .NET, которая по-Вашему может служить хорошим примером для изучения этих вещей.
Я всё как-то в области close source работал последнее время С ходу даже и не подскажу на что можно посмотреть так, чтобы понятно было.
*Кстати, совсем недавно я пытался починить одну проприетарную .NET/C# либу как раз такого толка — асинхронное сетевое I/O. У нас кончилась лицензия на апгрейды (давным-давно кончилась ), а тут клиент переехал на толстую многопроцессорную + многоядерную машинку, и вдруг полезли нездоровые глюки.
Так вот, я опух разбираться в её внутренностях. И это при наличии полных source'ов! Так за разумное время ничего толком и не раскопал Ну кроме того, что основная часть проблем была связана всё-таки с конфигурацией ОС
Здравствуйте, jedi, Вы писали:
J>Может тяжелое наследие С++
Если Вы раньше писали на С++, то обязательно прочтите это. Так, например, вы узнаете, что Thread.Abort()(.NET)и TerminateThread() (Win32) это не одно и тоже, поэтому можно не пичкать код событиями синхронизации,(Manual/AutoResetEvent) для корректного завершения потоков, как это было в С++(Event). А также разницу между lock() парой EnterCriticalSection(), LeaveCriticalSection ().
J>В частности, не совсем понятно как решается проблема отмены асинхронных вызовов.
Асинхронные операции такие, как BeginInvokе, BeginAccept и все остальные BeginXxx исполняются в потоке пула потоков и являются background потоками, так же как и их колбеки. После того, как такой поток был запущен, остановить его нельзя(можно, но это плохая идея, т.к по окончанию задачи такие потоки должны вернуться обратно в пул).
«Мы с тобой в чудеса не верим, Оттого их у нас не бывает…»
Здравствуйте, jedi, Вы писали:
_FR>>Если "остановка прослушивания" — штатная операция (а иначе и быть не может, ибо к остановке приводит явный вызов), то нет ничего страшного в том, что бы хранить специальный флаг, сообщающий о том, "остановлена ли прослушка".
J>…Вот только флагом (булевской переменной) здесь не обойтись. По крайней мере, я не вижу как это сделать?
private Timer timer;
private System.Threading.AutoResetEvent evt;
private bool timerAborted = false;private void OnTimer(object sender, ElapsedEventArgs args)
{
if(!timerAborted) {// Если timerAborted, то обрабатывать ничего и не надо
evt.Set(); // "Нормальная" обработка события}//if
}
public void RunTest()
{
using (evt = new System.Threading.AutoResetEvent(false))
using (timer = new Timer())
{
timerAborted = false;
timer.Elapsed += this.OnTimer;
timer.Interval = 10;
timer.Enabled = true;
evt.WaitOne(9, false);
timer.Enabled = false;
timerAborted = true;// на самом деле, гарантировать,
// что обработчик события не вызовется между
// "timer.Enabled = false;" тут нельзя, но оно и не нужно.
}
}
хотя, в данном конкретном случае с таймером, можно попросту вместо флага timerAborted проверять значение свойства timer.Enabled.
Help will always be given at Hogwarts to those who ask for it.
Здравствуйте, _FRED_, Вы писали:
_FR>хотя, в данном конкретном случае с таймером, можно попросту вместо флага timerAborted проверять значение свойства timer.Enabled.
Вы действительно считаете что это будет работать? Хинт: добавьте логирование исключения в консоль и попробуйте позапускать.
Здравствуйте, jedi, Вы писали:
_FR>>хотя, в данном конкретном случае с таймером, можно попросту вместо флага timerAborted проверять значение свойства timer.Enabled.
J>Вы действительно считаете что это будет работать? Хинт: добавьте логирование исключения в консоль и попробуйте позапускать.
Добавил. Только вместо System.Timers.Timer я взял System.Threading.Timer:
namespace test
{
using System;
using System.Diagnostics;
using System.Threading;
class Test
{
private Timer timer;
private AutoResetEvent evt;
//private bool timerAborted = false;public void RunTest() {
TimerCallback callback = state => {
try {
//if(!timerAborted) {
evt.Set();
//}//if
Debug.Print("Success");
} catch(ObjectDisposedException ex) {
Debug.Print("Exception: {0}", ex);
}//try
};
using(evt = new AutoResetEvent(false))
using(timer = new Timer(callback, null, 0, 10)) {
evt.WaitOne(9, false);
//timerAborted = true;
}
}
public static void Run() {
var test = new Test();
test.RunTest();
}
}
class Program
{
private static void Main(string[] args) {
while(true) {
Test.Run();
}
}
}
}
Так вот после раскоментирования timerAborted тест не валится Да и с чего бы?
Вообще, пример мог бы быть и таким:
using System;
using System.Diagnostics;
using System.Threading;
internal static class Program
{
private static void Main() {
while(true) {
bool timerAborted = false;
TimerCallback callback = state => {
try {
if(!timerAborted) {
((EventWaitHandle)state).Set();
}//if
} catch(ObjectDisposedException ex) {
Debug.Print("Exception: {0}", ex);
}//try
};
using(var evt = new AutoResetEvent(false))
using(var timer = new Timer(callback, evt, 0, 10)) {
evt.WaitOne(9, false);
//timerAborted = true;
}//using
}//while
}
}
Help will always be given at Hogwarts to those who ask for it.
Здравствуйте, drol, Вы писали:
D>Чтобы было удобней устраивать сложные внешние синхронизации (ну там через WaitForMultipleObjects() какие-нибудь), объект ожидания вытащили прямиком в IAsyncResult.AsyncWaitHandle: D>
D>var r = sock.BeginAccept(null, null);
D>r.AsyncWaitHandle.WaitOne();
D>var c = sock.EndAccept(r);
D>
D>Работает аналогично первому примеру.
Не знаю, есть ли это в .Net, но снятие асинхронного запроса в Win32 есть — CancelIO.
Здравствуйте, _FRED_, Вы писали:
_FR> using(var timer = new Timer(callback, evt, 0, 10)) {
Во-первых, Вы перепутали параметры конструктора таймера.
[msdn]
public Timer(TimerCallback callback, Object state, int dueTime, int period)
dueTime
The amount of time to delay before callback is invoked, in milliseconds. Specify Timeout..::.Infinite to prevent the timer from starting. Specify zero (0) to start the timer immediately.
period
The time interval between invocations of callback, in milliseconds. Specify Timeout..::.Infinite to disable periodic signaling.
[/msdn]
Таким образом, таймер у Вас файрится сразу же, еще до его диспоза. Поэтому, никакого исключения не происходит.
Во-вторых вы используете Debug.Print, т.е. судя по всему запускаете пример под дебаггером. Это в корне меняет поведение многопоточной программы.
В-третьих (это мой недочет) нужно поставить 10 миллисекунд в вейте: evt.WaitOne(10, false). Это намного повышает вероятность появления исключения. Еще лучше поставить и таймеру и вейту 1 мс, чтобы увеличить кол-во прогонов в единицу времени. Это тоже увеличт вероятность воспроизведения бага.
В результате имеем такой код:
namespace test
{
using System;
using System.Diagnostics;
using System.Threading;
class Test
{
private Timer timer;
private AutoResetEvent evt;
private bool timerAborted = false;
public void RunTest()
{
TimerCallback callback = state =>
{
try
{
if (!timerAborted)
{
// Context switch #1
evt.Set();
}
}
catch (ObjectDisposedException ex)
{
Console.WriteLine("Exception: {0}", ex);
}
};
using (evt = new AutoResetEvent(false))
using (timer = new Timer(callback, null, 1, 0))
{
evt.WaitOne(1, false);
// Context switch 2
timerAborted = true;
}
}
public static void Run()
{
var test = new Test();
test.RunTest();
}
}
class Program
{
private static void Main(string[] args)
{
while (true)
{
Test.Run();
}
}
}
}
Если его скомпилировать и запустить на исполнение (не под дебаггером), то на моей двухпроцессорной машине исключение происходит довольно регулярно (иногда — сразу после старта программы, иногда через пару минут).
Также вероятнось воспроизведения повышается, если запустить несколько копий программы.
Это типичный детский баг с потоками. Причем очень трудно воспроизводимый и приводящий к крешу "раз в месяц в
висимости от фазы луны". Конечно, приложения могут жить (и живут) с подобными багами десятилетиями. Но,
пардон, я не хочу быть писателем таких приложений.
Причина бага очень простая. Представьте что выполнение потока из пула (исполняющего код колбека таймера) прерывается в точке "Context switch #1" — после проверки флага. В этот момент исполнение главного потока
продолжается в точке "Context switch #2" — тут мы устанавливаем флаг и счастливо грожаем таймер и ивент.
Теперь, когда исполнение таймерного колбека возобновится, он обратится к убитому ивенту.
Сразу после отправки сообщения, допер как сделать баг 100% воспроизводимым.
Достаточно эмулировать переключение контекста сразу после проверки флага в колбеке. Вот так:
if (!timerAborted)
{
Thread.Sleep(0); // имитируем переключение контекста
evt.Set();
}