Отмена асинхронных вызовов
От: jedi Мухосранск  
Дата: 19.09.08 08:27
Оценка:
Всем привет.

Изучаю .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-ом объекта? Либо нужно городить дополнительную синхронизацию самому (очень не хочется).

Спасибо!
... << RSDN@Home 1.2.0 alpha rev. 0>>
Re: Отмена асинхронных вызовов
От: Аlexey  
Дата: 19.09.08 08:36
Оценка:
Здравствуйте, jedi, Вы писали:

J>Собственно вопрос состоит в следующем: есть ли стандартное средство, позволяющее дождаться завершения исполняющихся асинхронных вызовов перед dispos-ом объекта? Либо нужно городить дополнительную синхронизацию самому (очень не хочется).


J>Спасибо!


Есть такое средство
IAsyncResult ar = socket.BeginAccept();
ar.AsyncWaitHandle.WaitOne();
Re[2]: Отмена асинхронных вызовов
От: jedi Мухосранск  
Дата: 19.09.08 08:56
Оценка:
Здравствуйте, Аlexey, Вы писали:

А>Есть такое средство

А>
А>IAsyncResult ar = socket.BeginAccept();
А>ar.AsyncWaitHandle.WaitOne();
А>


Чудесное средство, только не для того случая, который я описал.
Еще раз — я хочу остановить сервер и отменить прослушку. Насколько я понял,
официальный способ — это Close. Далее, проблемы описаны в первом посте.
... << RSDN@Home 1.2.0 alpha rev. 0>>
Re: Отмена асинхронных вызовов
От: _FRED_ Черногория
Дата: 19.09.08 12:10
Оценка: 2 (2)
Здравствуйте, 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.
Re[2]: Отмена асинхронных вызовов
От: jedi Мухосранск  
Дата: 19.09.08 13:16
Оценка:
Здравствуйте, _FRED_, Вы писали:

_FR>Здравствуйте, jedi, Вы писали:


J>>В частности, не совсем понятно как решается проблема отмены асинхронных вызовов.

J>>К примеру, у меня есть слушающий сокет, которому я сказал BeginAccept.
J>>В какой-то момент, я решаю остановить прослушивание и вызываю socket.Close().
J>>Что произойдет, если как раз в этот момент исполняется код из accept callback-а?
J>>Ведь он может обращаться к объектам которые уже disposed, верно?

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


В общем, Вы правы. Вот только флагом (булевской переменной) здесь не обойтись. По крайней мере, я не вижу как это сделать?

Если не сложно, не могли бы Вы показать как это сделать в примере с таймером?
И еще = я знаю про Timer.Dispose(WaitHandle) но, к сожалению в том же сокете аналогов нет,
поэтому нужно более общее решение.
... << RSDN@Home 1.2.0 alpha rev. 0>>
Re: Отмена асинхронных вызовов
От: drol  
Дата: 20.09.08 15:59
Оценка:
Здравствуйте, jedi, Вы писали:

J>К примеру, у меня есть слушающий сокет, которому я сказал BeginAccept.

J>В какой-то момент, я решаю остановить прослушивание и вызываю socket.Close().
J>Что произойдет, если как раз в этот момент исполняется код из accept callback-а?
J>Ведь он может обращаться к объектам которые уже disposed, верно?

Disposed в этом случае может быть только один объект — оригинальный слушающий сокет. Ну получите исключение ObjectDisposedException при вызове EndAccept(). Пока проблем не вижу. Я что-то не понимаю ?

J>Следущий код, демонстрирует проблему (на примере таймера):


Э-э-э... Так Вам сокеты, или всё-таки что-то другое ?
Re[2]: Отмена асинхронных вызовов
От: jedi Мухосранск  
Дата: 20.09.08 16:14
Оценка:
Здравствуйте, drol, Вы писали:

D>Здравствуйте, jedi, Вы писали:


D>Disposed в этом случае может быть только один объект — оригинальный слушающий сокет.


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

D>Ну получите исключение ObjectDisposedException при вызове EndAccept(). Пока проблем не вижу. Я что-то не понимаю ?


Я как-то не привык, чтоб у меня такие исключения просто так летали. Может тяжелое наследие С++

D>Э-э-э... Так Вам сокеты, или всё-таки что-то другое ?


Пример с таймером — для простоты. На самом деле проблема общая, не только сокетов касается ...
... << RSDN@Home 1.2.0 alpha rev. 0>>
Re[3]: Отмена асинхронных вызовов
От: drol  
Дата: 20.09.08 18:49
Оценка:
Здравствуйте, 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-потоки...
Re[2]: Отмена асинхронных вызовов
От: drol  
Дата: 20.09.08 19:11
Оценка:
Здравствуйте, Аlexey, Вы писали:

J>>Собственно вопрос состоит в следующем: есть ли стандартное средство, позволяющее дождаться завершения исполняющихся асинхронных вызовов перед dispos-ом объекта? Либо нужно городить дополнительную синхронизацию самому (очень не хочется).


А>Есть такое средство

А>
А>IAsyncResult ar = socket.BeginAccept();
А>ar.AsyncWaitHandle.WaitOne();
А>

Это средство не поможет. В случае сокета, WaitOne() ожидает (если не вдаваться в подробности) не окончание исполнения делегата, а момент отправки делегата на исполнение.
Re[3]: Отмена асинхронных вызовов
От: jedi Мухосранск  
Дата: 20.09.08 19:17
Оценка:
Здравствуйте, drol, Вы писали:

D>Здравствуйте, Аlexey, Вы писали:


D>Это средство не поможет. В случае сокета, WaitOne() ожидает (если не вдаваться в подробности) не окончание исполнения делегата, а момент отправки делегата на исполнение.


Можно ссылку на документацию где это сказано? (хочется разобраться в деталях)
... << RSDN@Home 1.2.0 alpha rev. 0>>
Re[4]: Отмена асинхронных вызовов
От: jedi Мухосранск  
Дата: 20.09.08 19:17
Оценка:
Здравствуйте, drol, Вы писали:

D>*Да, и не забывайте, делегаты Timer'а и BeginXXX() работают в потоках из пула, а они все background-потоки...


Как все сложно... Кстати, не могли бы Вы ткнуть в какую-нибудь опенсорсную реализацию сервера на .NET, которая по-Вашему может служить хорошим примером для изучения этих вещей.
... << RSDN@Home 1.2.0 alpha rev. 0>>
Re[4]: Отмена асинхронных вызовов
От: drol  
Дата: 20.09.08 20:22
Оценка:
Здравствуйте, 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 Иногда требуется

Ну и, наконец, то что мы обсуждали с самого начала: делегат, вызывающийся в некотором потоке из пула, после окончания отработки исходного (настоящего) асинхронного вызова.
*Надеюсь стало яснее
Re[5]: Отмена асинхронных вызовов
От: drol  
Дата: 20.09.08 20:44
Оценка:
Здравствуйте, jedi, Вы писали:

J>Как все сложно... Кстати, не могли бы Вы ткнуть в какую-нибудь опенсорсную реализацию сервера на .NET, которая по-Вашему может служить хорошим примером для изучения этих вещей.

Я всё как-то в области close source работал последнее время С ходу даже и не подскажу на что можно посмотреть так, чтобы понятно было.

*Кстати, совсем недавно я пытался починить одну проприетарную .NET/C# либу как раз такого толка — асинхронное сетевое I/O. У нас кончилась лицензия на апгрейды (давным-давно кончилась ), а тут клиент переехал на толстую многопроцессорную + многоядерную машинку, и вдруг полезли нездоровые глюки.
Так вот, я опух разбираться в её внутренностях. И это при наличии полных source'ов! Так за разумное время ничего толком и не раскопал Ну кроме того, что основная часть проблем была связана всё-таки с конфигурацией ОС
Re: Отмена асинхронных вызовов
От: meerius Канада  
Дата: 20.09.08 20:52
Оценка: 4 (1)
Здравствуйте, jedi, Вы писали:

J>Может тяжелое наследие С++


Если Вы раньше писали на С++, то обязательно прочтите это. Так, например, вы узнаете, что Thread.Abort()(.NET)и TerminateThread() (Win32) это не одно и тоже, поэтому можно не пичкать код событиями синхронизации,(Manual/AutoResetEvent) для корректного завершения потоков, как это было в С++(Event). А также разницу между lock() парой EnterCriticalSection(), LeaveCriticalSection ().

J>В частности, не совсем понятно как решается проблема отмены асинхронных вызовов.


Асинхронные операции такие, как BeginInvokе, BeginAccept и все остальные BeginXxx исполняются в потоке пула потоков и являются background потоками, так же как и их колбеки. После того, как такой поток был запущен, остановить его нельзя(можно, но это плохая идея, т.к по окончанию задачи такие потоки должны вернуться обратно в пул).
«Мы с тобой в чудеса не верим, Оттого их у нас не бывает…»
Re[3]: Отмена асинхронных вызовов
От: _FRED_ Черногория
Дата: 22.09.08 05:54
Оценка:
Здравствуйте, 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.
Re[4]: Отмена асинхронных вызовов
От: jedi Мухосранск  
Дата: 22.09.08 09:25
Оценка:
Здравствуйте, _FRED_, Вы писали:

_FR>хотя, в данном конкретном случае с таймером, можно попросту вместо флага timerAborted проверять значение свойства timer.Enabled.


Вы действительно считаете что это будет работать? Хинт: добавьте логирование исключения в консоль и попробуйте позапускать.
... << RSDN@Home 1.2.0 alpha rev. 0>>
Re[5]: Отмена асинхронных вызовов
От: _FRED_ Черногория
Дата: 22.09.08 11:14
Оценка:
Здравствуйте, 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.
Re[5]: Отмена асинхронных вызовов
От: Pavel Dvorkin Россия  
Дата: 22.09.08 11:27
Оценка: 9 (1)
Здравствуйте, 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.
With best regards
Pavel Dvorkin
Re[6]: Отмена асинхронных вызовов
От: jedi Мухосранск  
Дата: 22.09.08 12:05
Оценка: +1
Здравствуйте, _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" — тут мы устанавливаем флаг и счастливо грожаем таймер и ивент.
Теперь, когда исполнение таймерного колбека возобновится, он обратится к убитому ивенту.
... << RSDN@Home 1.2.0 alpha rev. 0>>
Re[7]: Отмена асинхронных вызовов
От: jedi Мухосранск  
Дата: 22.09.08 12:10
Оценка:
Сразу после отправки сообщения, допер как сделать баг 100% воспроизводимым.
Достаточно эмулировать переключение контекста сразу после проверки флага в колбеке. Вот так:


if (!timerAborted)
{
        Thread.Sleep(0); // имитируем переключение контекста
    evt.Set();
}
... << RSDN@Home 1.2.0 alpha rev. 0>>
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.