Не понимаю поведение GC
От: FlyDN  
Дата: 07.11.05 22:06
Оценка:
В следующем коде "Finalizer" мы не увидим никогда, а почему?

    class Program
    {
        private class Test
        {
            Timer _timer;

            public Test()
            {
                _timer = new Timer(new TimerCallback(TimerCallbackFunc), null, 0, 0);
            }

            ~Test()
            {
                Console.WriteLine("Finalizer");
            }

            private void TimerCallbackFunc(object state)
            {

            }
        }

        static void Main()
        {
            new Test();

            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            Console.ReadLine();
        }
    }
... << RSDN@Home 1.1.4 beta 7 rev. 447>>
Re: Не понимаю поведение GC
От: Mab Россия http://shade.msu.ru/~mab
Дата: 07.11.05 22:21
Оценка:
Здравствуйте, FlyDN, Вы писали:

FDN>В следующем коде "Finalizer" мы не увидим никогда, а почему?

Ну например я вижу (FW 2.0). Только после нажатия enter.
Почему не вызывается Finalize для таймера
От: Mab Россия http://shade.msu.ru/~mab
Дата: 07.11.05 22:29
Оценка: 72 (7) +1
#Имя: FAQ.dotnet.timer.nofinalize
Здравствуйте, FlyDN, Вы писали:

FDN>В следующем коде "Finalizer" мы не увидим никогда, а почему?

Ответ на данный вопрос скрыт внути класса Timer. Если, скажем, сделать ему Dispose, то наблюдать вызов финалайзера мы начинаем в ождидаемом месте.

А дело все в том, что все созданные таймеры регистрируются в VM. Код из Рефлектора:
public Timer(TimerCallback callback)
{
      int num1 = -1;
      int num2 = -1;
      StackCrawlMark mark1 = StackCrawlMark.LookForMyCaller;
      this.TimerSetup(callback, this, (uint) num1, (uint) num2, ref mark1);
}

Далее:
private void TimerSetup(TimerCallback callback, object state, uint dueTime, uint period, ref StackCrawlMark stackMark)
{
      this.timerBase = new TimerBase();
      this.timerBase.AddTimer(callback, state, dueTime, period, ref stackMark);
}

И наконец
internal void AddTimer(TimerCallback callback, object state, uint dueTime, uint period, ref StackCrawlMark stackMark)
{
      if (callback == null)
      {
            throw new ArgumentNullException("TimerCallback");
      }
      _TimerCallback callback1 = new _TimerCallback(callback, state, stackMark);
      state = callback1;
      this.AddTimerNative(state, dueTime, period, ref stackMark);
      this.timerDeleted = 0;
}

Здесь
[MethodImpl(MethodImplOptions.InternalCall)]
private extern void AddTimerNative(object state, uint dueTime, uint period, ref StackCrawlMark stackMark);

так что для того, чтобы отследить вызов дальше, придется копаться в C++-коде виртуальной машины (например, в Роторе).

Итак, глобальная таблица косвенно держит делегат, который держит экземпляр Test.

Мораль: не пренебрегайте вызовом Dispose. Иначе некоторе финалайзеры у вас вызовутся лишь при app domain shutdown
Re[2]: Не понимаю поведение GC
От: FlyDN  
Дата: 07.11.05 22:32
Оценка:
Здравствуйте, Mab, Вы писали:

Mab>Ну например я вижу (FW 2.0). Только после нажатия enter.


Ага, после нажатия enter приложение закрывается.
А почему Finalize не вызывается до этого?
... << RSDN@Home 1.1.4 beta 7 rev. 447>>
Re[2]: Не понимаю поведение GC
От: FlyDN  
Дата: 07.11.05 23:06
Оценка:
Здравствуйте, Mab, Вы писали:

Mab>Итак, глобальная таблица косвенно держит делегат, который держит экземпляр Test.


Mab>Мораль: не пренебрегайте вызовом Dispose. Иначе некоторе финалайзеры у вас вызовутся лишь при app domain shutdown


Где то так я и думал.

MSDN говорит.

As long as you are using a Timer, you must keep a reference to it. As with any managed object, a Timer is subject to garbage collection when there are no references to it. The fact that a Timer is still active does not prevent it from being collected.


И если поправить код:

  public Test()
  {
    // _timer = new Timer(new TimerCallback(TimerCallbackFunc), null, 0, 0);
         Timer timer = new Timer(new TimerCallback(TimerCallbackFunc), null, 0, 0);
  }


То после сборки мусора timer по идее должен собраться GC, но и после этого обьект продолжает жыть. То есть Timer в финализаторе не удаляет ссылку.Не хорошо.
Действительно, нужно звать Dispose всегда, иначе получим MemoryLeak на ровном месте.
... << RSDN@Home 1.1.4 beta 7 rev. 447>>
Re[3]: Не понимаю поведение GC
От: Mab Россия http://shade.msu.ru/~mab
Дата: 07.11.05 23:14
Оценка: 8 (2)
Здравствуйте, FlyDN, Вы писали:

FDN>MSDN говорит.

Увы, но действительно говорит он неправду И приведенный пример подтвреждает это.

FDN>То есть Timer в финализаторе не удаляет ссылку.

Да нет, Timer здесь не виноват. Это проблема дизайна. Зарегистрированный таймер получает ссылку от GC root-а. Соответственно, пока не будет Dispose он будет жить.

Для доказательства запустил пример под MemoryProfiler. Он показывает такую цепочку ссылок до Test:
System.Threading    TimerCallback
System.Threading    _TimerCallback
<root>

А вот точка создания _TimerCallback:
TimerBase.AddTimer(TimerCallback, object, uint, uint, StackCrawlMark&)
Timer.TimerSetup(TimerCallback, object, uint, uint, StackCrawlMark&)
Timer..ctor(TimerCallback, object, int, int)
Program.Test..ctor()
Program.Main()
Re[4]: Не понимаю поведение GC
От: FlyDN  
Дата: 07.11.05 23:34
Оценка:
Здравствуйте, Mab, Вы писали:

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


FDN>>MSDN говорит.

Mab>Увы, но действительно говорит он неправду И приведенный пример подтвреждает это.

FDN>>То есть Timer в финализаторе не удаляет ссылку.

Mab>Да нет, Timer здесь не виноват. Это проблема дизайна. Зарегистрированный таймер получает ссылку от GC root-а. Соответственно, пока не будет Dispose он будет жить.

Странно рефлектором посмотрел код Timer.Dispose()

public void Dispose()
{
      this.timerBase.Dispose();
}


А вот код TimerBase.Dispose():


[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
public void Dispose()
{
    bool flag1 = false;
    RuntimeHelpers.PrepareConstrainedRegions();
    try
    {
    }
    finally
    {
        do
        {
            if (Interlocked.CompareExchange(ref this.m_lock, 1, 0) == 0)
            {
                flag1 = true;
                try
                {
                            this.DeleteTimerNative(Win32Native.NULL);
                }
                finally
                {
                            this.m_lock = 0;
                }
            }
            Thread.SpinWait(1);
        }
        while (!flag1);
        GC.SuppressFinalize(this);
    }
}


А это код TimerBase.Finalize():
~TimerBase()
{
    bool flag1 = false;
    do
    {
        if (Interlocked.CompareExchange(ref this.m_lock, 1, 0) == 0)
        {
            flag1 = true;
            try
            {
                        this.DeleteTimerNative(Win32Native.NULL);
            }
            finally
            {
                        this.m_lock = 0;
            }
        }
        Thread.SpinWait(1);
    }
    while (!flag1);
}


Отличаются они только выделенным. Получается при вызове Dispose() ссылка на обьект обнуляется а после вызова Finalize() нет
... << RSDN@Home 1.1.4 beta 7 rev. 447>>
Re[5]: Не понимаю поведение GC
От: Mab Россия http://shade.msu.ru/~mab
Дата: 07.11.05 23:37
Оценка: +2
Здравствуйте, FlyDN, Вы писали:

FDN>Отличаются они только выделенным. Получается при вызове Dispose() ссылка на обьект обнуляется а после вызова Finalize() нет

Фишка в том, что вызов Finalize не может произойти в принципе, поскольку на таймер остается ссылка от GC root.
Re[6]: Не понимаю поведение GC
От: FlyDN  
Дата: 07.11.05 23:41
Оценка:
Здравствуйте, Mab, Вы писали:

FDN>>Отличаются они только выделенным. Получается при вызове Dispose() ссылка на обьект обнуляется а после вызова Finalize() нет

Mab>Фишка в том, что вызов Finalize не может произойти в принципе, поскольку на таймер остается ссылка от GC root.

А смысл тогда в TimerBase обьявлять Finalize?
... << RSDN@Home 1.1.4 beta 7 rev. 447>>
Re[7]: Не понимаю поведение GC
От: Mab Россия http://shade.msu.ru/~mab
Дата: 07.11.05 23:44
Оценка:
Здравствуйте, FlyDN, Вы писали:

FDN>А смысл тогда в TimerBase обьявлять Finalize?

Вы меня спрашиваете? Возможно автор данного кода попросту забыл о том, что ссылка остается. И сделал copy-paste для финалайзера.
Re[8]: Не понимаю поведение GC
От: FlyDN  
Дата: 07.11.05 23:57
Оценка:
Здравствуйте, Mab, Вы писали:

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


FDN>>А смысл тогда в TimerBase обьявлять Finalize?

Mab>Вы меня спрашиваете?
Нет, парней из MS, зная что они этого никогда не увидят
Да, тему я наверное не правильно назвал, GC тут совсем не причем.
... << RSDN@Home 1.1.4 beta 7 rev. 447>>
Re[2]: Не понимаю поведение GC
От: Аноним  
Дата: 08.11.05 10:24
Оценка: +1
Здравствуйте, Mab, Вы писали:

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


FDN>>В следующем коде "Finalizer" мы не увидим никогда, а почему?

Mab>Ответ на данный вопрос скрыт внути класса Timer. Если, скажем, сделать ему Dispose, то наблюдать вызов финалайзера мы начинаем в ождидаемом месте.

Mab>А дело все в том, что все созданные таймеры регистрируются в VM. Код из Рефлектора:


Что то ты перемудрил. Ничего нигде не регистрируется. Просто когда в мы создаем Таймер (или любой неуправляемый объект, в процессе инициализации которого используется callback-функция) мы не можем напрямую передавать объект делегата. Для этого передается его указатель.Но при такой передаче есть большая вероятность, что делегат может быть собран GC. Тогда при вызове из native кода мы можем получить неопределенный результат.
Не понимаю поведение GC
От: Аноним  
Дата: 08.11.05 11:40
Оценка:
Вы случайно не в дебаге это компилировали?
Если да, то в чем вопрос?
shura


данное сообщение получено с www.gotdotnet.ru
ссылка на оригинальное сообщение
Re[3]: Не понимаю поведение GC
От: Mab Россия http://shade.msu.ru/~mab
Дата: 08.11.05 11:53
Оценка: 6 (1)
А>Ничего нигде не регистрируется.
Это утверждение не верно. Регистрация происходит в глобальной таблице хэндлов на объекты.

А>Просто когда в мы создаем Таймер (или любой неуправляемый объект, в процессе инициализации которого используется callback-функция) мы не можем напрямую передавать объект делегата. Для этого передается его указатель.


Не надо путать вызовы через pinvoke и вызовы методов через MethodImplOptions.InternalCall. В первом случае обязательно создание unmanaged thunks (хотя бы через Marshal.GetFunctionPointerForDelegate). Во второй случае вызов уходит непосредственно в потроха виртуальной машины. Поддержание целостности ссылок при таком вызове -- ее проблема, которую она успешно решеает.

А>Но при такой передаче есть большая вероятность, что делегат может быть собран GC. Тогда при вызове из native кода мы можем получить неопределенный результат.

Не очень понятно, к чему это написано. Если имелось в виду, что иногда такие вызовы приведут к каким-то недопустимым действиям, то это неправда.

Для доказаетельства мне придется продемонстрировать код виртуальной машины из Ротора. Раз на слово мне верить не хотят

Здесь описан обработчик InternalCall-а:
static
ECFunc gTimerFuncs[] =
{
    {FCFuncElement("ChangeTimerNative", NULL, (LPVOID)TimerNative::CorChangeTimer)},
    {FCFuncElement("DeleteTimerNative", NULL, (LPVOID)TimerNative::CorDeleteTimer)},
    {FCFuncElement("AddTimerNative", NULL, (LPVOID)TimerNative::CorCreateTimer)},
    {NULL, NULL, NULL}
};


А вот он сам:
FCIMPL6(VOID, TimerNative::CorCreateTimer, TimerBaseNative* pThisUNSAFE, Object* delegateUNSAFE, Object* stateUNSAFE, INT32 dueTime, INT32 period, StackCrawlMark* stackMark)
{
    struct _gc
    {
        TIMERREF pThis;
        OBJECTREF delegate;
        OBJECTREF state;
    } gc;
    gc.pThis = (TIMERREF) pThisUNSAFE;
    gc.delegate = (OBJECTREF) delegateUNSAFE;
    gc.state = (OBJECTREF) stateUNSAFE;
    HELPER_METHOD_FRAME_BEGIN_NOPOLL();
    GCPROTECT_BEGIN(gc);
    HELPER_METHOD_POLL();
    //-[autocvtpro]-------------------------------------------------------

    THROWSCOMPLUSEXCEPTION();

    Thread* pCurThread = GetThread();
    _ASSERTE( pCurThread);

    AppDomain* appDomain = pCurThread->GetDomain();
    _ASSERTE(appDomain);

    DelegateInfo* delegateInfo = DelegateInfo::MakeDelegateInfo(appDomain,
                                                                gc.delegate,                                                                
                                                                gc.state,
                                                                NULL,
                                                                NULL);
    
...

Отмечу, что в метод передатеся самый что ни на есть настоящий указатели на объекты. Удержанием этих указателей от сбора занимается макрос GCPROTECT_BEGIN(gc):
//------------------------------------------------------------------------
// These macros GC-protect OBJECTREF pointers on the EE's behalf.
// In between these macros, the GC can move but not discard the protected
// objects. If the GC moves an object, it will update the guarded OBJECTREF's.
// Typical usage:
//
//   OBJECTREF or = <some valid objectref>;
//   GCPROTECT_BEGIN(or);
//
//   ...<do work that can trigger GC>...
//
//   GCPROTECT_END();
//
//
// These macros can also protect multiple OBJECTREF's if they're packaged
// into a structure:
//
//   struct xx {
//      OBJECTREF o1;
//      OBJECTREF o2;
//   } gc;
//
//   GCPROTECT_BEGIN(gc);
//   ....
//   GCPROTECT_END();


Идем далее. Происходит вызов DelegateInfo::MakeDelegateInfo. Вот что он делает:
/*****************************************************************************************************/
DelegateInfo *DelegateInfo::MakeDelegateInfo(AppDomain *pAppDomain, 
                                             OBJECTREF delegate,                                              
                                             OBJECTREF state,
                                             OBJECTREF waitEvent,
                                             OBJECTREF registeredWaitHandle)
{
    THROWSCOMPLUSEXCEPTION();
    DelegateInfo* delegateInfo = (DelegateInfo*) ThreadpoolMgr::GetRecycledMemory(ThreadpoolMgr::MEMTYPE_DelegateInfo);
    _ASSERTE(delegateInfo);
    if (NULL == delegateInfo)
        COMPlusThrow(kOutOfMemoryException);
    delegateInfo->m_appDomainId = pAppDomain->GetId();

    delegateInfo->m_delegateHandle = pAppDomain->CreateHandle(delegate);
    delegateInfo->m_stateHandle = pAppDomain->CreateHandle(state);
    delegateInfo->m_eventHandle = pAppDomain->CreateHandle(waitEvent);
    delegateInfo->m_registeredWaitHandle = pAppDomain->CreateHandle(registeredWaitHandle);
...

Вот мы и получили записи в глобальной таблице.
Re[4]: Не понимаю поведение GC
От: Аноним  
Дата: 08.11.05 12:48
Оценка:
Здравствуйте, Mab, Вы писали:

А>>Ничего нигде не регистрируется.

Mab>Это утверждение не верно. Регистрация происходит в глобальной таблице хэндлов на объекты.

Если бы ты знал, сколько у VM таких таблиц, ты бы не разбрасывался словом "глобальный" так легко

А>>Просто когда в мы создаем Таймер (или любой неуправляемый объект, в процессе инициализации которого используется callback-функция) мы не можем напрямую передавать объект делегата. Для этого передается его указатель.


Mab>Не надо путать вызовы через pinvoke и вызовы методов через MethodImplOptions.InternalCall.


Не надо, вот поэтому я их и не путаю . Кстати, встречный вопрос. А в данном случае между ними какая разница? Могу дать сразу ответ, но это не интересно.

Mab>В первом случае обязательно создание unmanaged thunks (хотя бы через Marshal.GetFunctionPointerForDelegate).


GetFunctionPointerForDelegate — это игра с GCHandle. Ничего больше.

Mab>Во второй случае вызов уходит непосредственно в потроха виртуальной машины. Поддержание целостности ссылок при таком вызове -- ее проблема, которую она успешно решеает.


Да ничего она не решает. Она делает тоже самое, что и управляемый код. Надеюсь не нужно обяснять, что VM — это unmanaged приложение, и для нее определена такая же политика, как для обычного приложения. GC наплевать, кто там держит ссылку.

А>>Но при такой передаче есть большая вероятность, что делегат может быть собран GC. Тогда при вызове из native кода мы можем получить неопределенный результат.

Mab>Не очень понятно, к чему это написано. Если имелось в виду, что иногда такие вызовы приведут к каким-то недопустимым действиям, то это неправда.

Ага, а AccessViolationException — это у нас нормальное поведение программы

Mab>Для доказаетельства мне придется продемонстрировать код виртуальной машины из Ротора. Раз на слово мне верить не хотят



Mab>А вот он сам:

Mab>
Mab>FCIMPL6(VOID, TimerNative::CorCreateTimer, TimerBaseNative* pThisUNSAFE, Object* delegateUNSAFE, Object* stateUNSAFE, INT32 dueTime, INT32 period, StackCrawlMark* stackMark)
Mab>{
Mab>    struct _gc
Mab>    {
Mab>        TIMERREF pThis;
Mab>        OBJECTREF delegate;
Mab>        OBJECTREF state;
Mab>    } gc;
Mab>    gc.pThis = (TIMERREF) pThisUNSAFE;
Mab>    gc.delegate = (OBJECTREF) delegateUNSAFE;
Mab>    gc.state = (OBJECTREF) stateUNSAFE;
Mab>    HELPER_METHOD_FRAME_BEGIN_NOPOLL();
Mab>    GCPROTECT_BEGIN(gc);
Mab>    HELPER_METHOD_POLL();
Mab>    //-[autocvtpro]-------------------------------------------------------

Mab>    THROWSCOMPLUSEXCEPTION();

Mab>    Thread* pCurThread = GetThread();
Mab>    _ASSERTE( pCurThread);

Mab>    AppDomain* appDomain = pCurThread->GetDomain();
Mab>    _ASSERTE(appDomain);

Mab>    DelegateInfo* delegateInfo = DelegateInfo::MakeDelegateInfo(appDomain,
Mab>                                                                gc.delegate,                                                                
Mab>                                                                gc.state,
Mab>                                                                NULL,
Mab>                                                                NULL);
    
Mab>...
Mab>

Mab>Отмечу, что в метод передатеся самый что ни на есть настоящий указатели на объекты. Удержанием этих указателей от сбора занимается макрос GCPROTECT_BEGIN(gc):

Верно. Теперь осталось понять, зачем все это нужно было. Ответ — ищи в моей предыдущем топике. Ты все слишком перемудрил, нужно мыслить проще.


Mab>Идем далее. Происходит вызов DelegateInfo::MakeDelegateInfo. Вот что он делает:

Mab>
Mab>/*****************************************************************************************************/
Mab>DelegateInfo *DelegateInfo::MakeDelegateInfo(AppDomain *pAppDomain, 
Mab>                                             OBJECTREF delegate,                                              
Mab>                                             OBJECTREF state,
Mab>                                             OBJECTREF waitEvent,
Mab>                                             OBJECTREF registeredWaitHandle)
Mab>{
Mab>    THROWSCOMPLUSEXCEPTION();
Mab>    DelegateInfo* delegateInfo = (DelegateInfo*) ThreadpoolMgr::GetRecycledMemory(ThreadpoolMgr::MEMTYPE_DelegateInfo);
Mab>    _ASSERTE(delegateInfo);
Mab>    if (NULL == delegateInfo)
Mab>        COMPlusThrow(kOutOfMemoryException);
Mab>    delegateInfo->m_appDomainId = pAppDomain->GetId();

Mab>    delegateInfo->m_delegateHandle = pAppDomain->CreateHandle(delegate);
Mab>    delegateInfo->m_stateHandle = pAppDomain->CreateHandle(state);
Mab>    delegateInfo->m_eventHandle = pAppDomain->CreateHandle(waitEvent);
Mab>    delegateInfo->m_registeredWaitHandle = pAppDomain->CreateHandle(registeredWaitHandle);
Mab>...
Mab>

Mab>Вот мы и получили записи в глобальной таблице.

Ты не поверишь, но здесь мы только создали делегат. Ничего больше
Re[5]: Не понимаю поведение GC
От: Mab Россия http://shade.msu.ru/~mab
Дата: 08.11.05 15:34
Оценка:
А>Если бы ты знал, сколько у VM таких таблиц, ты бы не разбрасывался словом "глобальный" так легко
А я что, имею честь беседовать с одним из авторов .NET VM?

Данная таблица глобальна в пределах домена.

А>А в данном случае между ними какая разница?

Хотя бы в отсутствии маршаллинга.

А>GetFunctionPointerForDelegate — это игра с GCHandle. Ничего больше.

Ну и к чему это замечание относится? CreateHandle, к которому приводит регистрация таймера, имеет сходный эффект.

А>Да ничего она не решает. Она делает тоже самое, что и управляемый код.

Смысл данного утверждения непонятен.

А>Надеюсь не нужно обяснять, что VM — это unmanaged приложение, и для нее определена такая же политика, как для обычного приложения.

В каком смысле "политика"?

А>GC наплевать, кто там держит ссылку.

GC не телепат. О том, что ссылку кто-то держит, он узнает лишь если информация о владении сслылкой явно оформлена. Для случая кода, порождаемого JIT-ом, такие таблицы строятся автоматически. А вот для случая unmanaged-кода виртуальной матшны их нет и приходится предпринимать специальные усилия (GCPROTECT).


А>Ага, а AccessViolationException — это у нас нормальное поведение программы

Я наверное тупой, но не понимаю смысл юмора. Увы.

А>Ты все слишком перемудрил, нужно мыслить проще.

Мыслить нужно проще. Но одновременно не нужно делать ложных утверждений. А именно, утверждение о том, что создание таймера не приводит к регистрации нового GC-рута, не верно. Или может я неправильно тебя понял?

А>Ты не поверишь, но здесь мы только создали делегат. Ничего больше

Ты не поверишь, но ты не прав.
По твоему, делегат -- это структура DelegateInfo, содержащая хэндлы типа m_delegateHandle или m_stateHandle? Ну-ну
DelegateInfo не имеет отношения к экзеплярам класса Delegate. Для того, чтобы понять, как устроены делегаты, советую изучить файлы delegate.cs,comdelegate.h/cpp.
Re: Не понимаю поведение GC
От: FlyDN  
Дата: 09.11.05 12:09
Оценка:
Здравствуйте, erigami, Вы писали:

E>Вы случайно не в дебаге это компилировали?

E>Если да, то в чем вопрос?

Нет не в дебаге. А вопрос был в том кто держал ссылку не обьект, когда видимых глобальных ссылок вроде бы нет.
На него и ответил Mab.
... << RSDN@Home 1.1.4 beta 7 rev. 447>>
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.