7

序文: 私は問題を解決する方法を知っています. なぜ発生するのか知りたいです。質問を上から下まで読んでください。

誰もが (当然のことながら) 知っているように、イベント ハンドラーを追加すると、C# でメモリ リークが発生する可能性があります。イベント ハンドラのメモリ リークを回避する理由と方法を参照してください。

一方、オブジェクトのライフ サイクルは類似または関連していることが多く、イベント ハンドラの登録解除は必要ありません。次の例を検討してください。

using System;

public class A
{
    private readonly B b;

    public A(B b)
    {
        this.b = b;
        b.BEvent += b_BEvent;
    }

    private void b_BEvent(object sender, EventArgs e)
    {
        // NoOp
    }

    public event EventHandler AEvent;
}

public class B
{
    private readonly A a;

    public B()
    {
        a = new A(this);
        a.AEvent += a_AEvent;
    }

    private void a_AEvent(object sender, EventArgs e)
    {
        // NoOp
    }

    public event EventHandler BEvent;
}

internal class Program
{
    private static void Main(string[] args)
    {
        B b = new B();

        WeakReference weakReference = new WeakReference(b);
        b = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();

        bool stillAlive = weakReference.IsAlive; // == false
    }
}

AイベントをB介して暗黙的に相互参照しますが、GC はそれらを削除できます (参照カウントではなく、マークアンドスイープを使用しているため)。

しかし、次の同様の例を考えてみましょう:

using System;
using System.Timers;

public class C
{
    private readonly Timer timer;

    public C()
    {
        timer = new Timer(1000);
        timer.Elapsed += timer_Elapsed;
        timer.Start(); // (*)
    }

    private void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        // NoOp
    }
}

internal class Program
{
    private static void Main(string[] args)
    {
        C c = new C();

        WeakReference weakReference = new WeakReference(c);
        c = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        bool stillAlive = weakReference.IsAlive; // == true !
    }
}

CGC がオブジェクトを削除できないのはなぜですか? Timer がオブジェクトを存続させるのはなぜですか? タイマーは、タイマー機構の「隠された」参照 (静的参照など) によって維持されていますか?

(*) 注意: タイマーが作成されただけで、開始されていない場合、問題は発生しません。開始して後で停止しても、イベント ハンドラーが登録解除されていない場合、問題は解決しません。

4

6 に答える 6

5

タイマー ロジックは、OS の機能に依存します。実際にイベントを発生させるのは OS です。OSはCPU割り込みを使用してそれを実装します。

OS API (別名 Win32) は、いかなる種類のオブジェクトへの参照も保持しません。タイマーイベントが発生したときに呼び出す必要がある関数のメモリアドレスを保持します。.NET GC には、そのような「参照」を追跡する方法がありません。その結果、低レベルのイベントからサブスクライブしなくても、タイマー オブジェクトを収集できました。OSがとにかくそれを呼び出そうとし、奇妙なメモリアクセス例外でクラッシュするため、これは問題です. そのため、.NET Framework はそのようなタイマー オブジェクトをすべて静的に参照されるオブジェクトに保持し、サブスクライブを解除した場合にのみそのコレクションから削除します。

SOS.dll を使用してオブジェクトのルートを見ると、次の図が得られます。

!GCRoot 022d23fc
HandleTable:
    001813fc (pinned handle)
    -> 032d1010 System.Object[]
    -> 022d2528 System.Threading.TimerQueue
    -> 022d249c System.Threading.TimerQueueTimer
    -> 022d2440 System.Threading.TimerCallback
    -> 022d2408 System.Timers.Timer
    -> 022d2460 System.Timers.ElapsedEventHandler
    -> 022d23fc TimerTest.C

次に、dotPeek などで System.Threading.TimerQueue クラスを見ると、それがシングルトンとして実装され、タイマーのコレクションを保持していることがわかります。

それがどのように機能するかです。残念ながら、MSDN のドキュメントはそれについて明確ではありません。彼らは、それが IDisposable を実装している場合、問題なく破棄する必要があると想定していました。

于 2013-01-08T17:59:16.427 に答える
3

タイマーは、タイマー機構の「隠された」参照 (静的参照など) によって維持されていますか?

はい。これは CLR に組み込まれており、参照ソースまたは逆コンパイラ、Timer クラスのプライベート「Cookie」フィールドを使用すると、そのトレースを確認できます。これは、タイマーを実際に実装する System.Threading.Timer コンストラクター (「状態」オブジェクト) に 2 番目の引数として渡されます。

CLR は、有効なシステム タイマーのリストを保持し、状態オブジェクトへの参照を追加して、ガベージ コレクションが行われないようにします。これにより、Timer オブジェクトがリストにある限り、ガベージ コレクションが行われないことが保証されます。

したがって、System.Timers.Timer のガベージ コレクションを取得するには、その Stop() メソッドを呼び出すか、その Enabled プロパティを false に設定する必要があります。これにより、CLR はアクティブなタイマーのリストからシステム タイマーを削除します。これにより、状態オブジェクトへの参照も削除されます。これにより、タイマー オブジェクトがコレクションの対象になります。

明らかに、これは望ましい動作です。通常、タイマーがアクティブな間だけ消えて、タイマーの動作を停止することは望ましくありません。System.Threading.Timerを使用すると発生します。明示的に、または状態オブジェクトを使用して参照を保持しないと、コールバックの呼び出しが停止します。

于 2013-01-08T14:51:04.020 に答える
2

これはタイマーの実装方法に関連していると思います。Timer.Start() を呼び出すと、Timer.Enabled = true が設定されます。Timer.Enabled の実装を見てください。

public bool Enabled
{
    [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
    get
    {
        return this.enabled;
    }
    set
    {
        if (base.DesignMode)
        {
            this.delayedEnable = value;
            this.enabled = value;
        }
        else if (this.initializing)
        {
            this.delayedEnable = value;
        }
        else if (this.enabled != value)
        {
            if (!value)
            {
                if (this.timer != null)
                {
                    this.cookie = null;
                    this.timer.Dispose();
                    this.timer = null;
                }
                this.enabled = value;
            }
            else
            {
                this.enabled = value;
                if (this.timer == null)
                {
                    if (this.disposed)
                    {
                        throw new ObjectDisposedException(base.GetType().Name);
                    }
                    int dueTime = (int) Math.Ceiling(this.interval);
                    this.cookie = new object();
                    this.timer = new Timer(this.callback, this.cookie, dueTime, this.autoReset ? dueTime : 0xffffffff);
                }
                else
                {
                    this.UpdateTimer();
                }
            }
        }
    }
}

新しいタイマーが作成され、Cookie オブジェクトが渡されたように見えます (非常に奇妙です!)。その呼び出しパスをたどると、TimerHolder と TimerQueueTimer の作成を含む他の複雑なコードにつながります。ある時点で、Timer.Stop() または Timer.Enabled = false を呼び出すまで、Timer 自体の外部に保持されている参照が作成されることを期待しています。

私が投稿したコードはどれもそのような参照を作成しないため、これは決定的な答えではありません。しかし、そのようなことが起こっているのではないかと疑うほど、根底では複雑です。

Reflector (または類似のもの) をお持ちの場合は、こちらをご覧ください。:)

于 2013-01-08T13:50:30.563 に答える
1

Timer今も現役だから。( のイベント ハンドラは削除されませんTimer.Elapsed)。

適切に破棄する場合は、IDisposableインターフェイスを実装し、メソッド内のイベント ハンドラーを削除して、ブロックDisposeを使用するか、手動で呼び出します。問題は発生しません。usingDispose

 public class C : IDisposable  
 {
    ...

    void Dispose()
    {
      timer.Elapsed -= timer_elapsed;
    }
 }

その後

 C c = new C();

 WeakReference weakReference = new WeakReference(c);
 c.Dispose();
 c = null;
于 2013-01-08T13:37:53.367 に答える
0

問題はこの行から生じると思います。

c = null;

一般に、ほとんどの開発者は、オブジェクトをnullに等しくすると、オブジェクトがガベージコレクターによって削除されると考えています。しかし、そうではありません。実際、メモリ位置(cオブジェクトが作成される場所)への参照のみが削除されます。関連するメモリ位置への他の参照がある場合、オブジェクトは削除のマークが付けられません。この場合、Timerは関連するメモリ位置を参照しているため、オブジェクトはガベージコレクタによって削除されません。

于 2013-01-08T13:39:45.457 に答える
0

最初に Threading.Timer について話しましょう。内部的に、タイマーはコールバックを使用して TimerQueueTimer オブジェクトを構築し、状態は Timer ctor に渡されます (たとえば、new Threading.Timer(callback, state, xxx, xxx)。TimerQueueTimer は静的リストに追加されます。

コールバック メソッドと状態に "this" 情報がない場合 (コールバックに静的メソッドを使用し、状態に null を使用するなど)、Timer オブジェクトは参照がない場合に GC できます。一方、メンバーメソッドがコールバックに使用される場合、「this」を含むデリゲートは上記の静的リストに格納されます。そのため、「C」(この例では) オブジェクトがまだ参照されているため、Timer オブジェクトを GC することはできません。

ここで、Threading.Timer を内部的にラップする System.Timers.Timer に戻りましょう。前者が後者を構築する場合、System.Timers.Timer メンバー メソッドが使用されるため、System.Timers.Timer オブジェクトを GC できないことに注意してください。

于 2015-11-13T08:23:34.910 に答える