GC.KeepAlive to preserve a context

Question

I have a class which is is a simple wrapper for WNetUseConnection

Here is an implementation (just for reference):

internal class RemoteFileSystemContext : IDisposable
{
    private readonly string _remoteUnc;
    private bool _isConnected;
    public RemoteFileSystemContext(string remoteUnc, string username, string password, bool promptUser)
    {
        if (WindowsNetworking.TryConnectToRemote(remoteUnc, username, password, promptUser))
        {
            _isConnected = true;
            _remoteUnc = remoteUnc;
        }
        else
        {
            GC.SuppressFinalize(this);
        }
    }

    public void Dispose()
    {
        Dispose(true);
    }

    ~RemoteFileSystemContext()
    {
        Dispose(false);
    }

    private void Dispose(bool isDisposing)
    {
        if (!_isConnected)
            return;
        _isConnected = false;
        if (isDisposing)
        {
            GC.SuppressFinalize(this);
        }
        WindowsNetworking.DisconnectRemote(_remoteUnc);
    }
}

and here is usage:

using (var context = WindowsNetworking.CreateRemoteContext(storagePath, login, pass))
{
    // do something with storagePath
    GC.KeepAlive(context);
}

The question is if I should write GC.KeepAlive(context) or not? I mean I didn't write code like this until I read an article (about AsyncLock, but now I can't find a link), and now I'm not sure if GC can call a finalizer before this method finishes. Theoretically, it should use Dispose in finally section of using, but this article was written by a smart guy, so I'm not sure now.


Just in case, I provide code for referenced class:

public static class WindowsNetworking
{
    public static bool TryConnectToRemote(string remoteUnc, string username, string password, bool promptUser = false)
    {
        bool isUnc = remoteUnc != null && remoteUnc.Length >= 2 && remoteUnc[0] == '\\' && remoteUnc[1] == '\\';
        if (!isUnc)
        {
            return false;
        }
        ConnectToRemote(remoteUnc, username, password, promptUser);
        return true;
    }

    public static IDisposable CreateRemoteContext(string remoteUnc, string username, string password, bool promptUser = false)
    {
        return new RemoteFileSystemContext(remoteUnc, username, password, promptUser);
    }

    public static void DisconnectRemote(string remoteUNC)
    {
        var ret = (NetworkError) WNetCancelConnection2(remoteUNC, CONNECT_UPDATE_PROFILE, false);
        if (ret != NetworkError.NO_ERROR)
        {
            throw new Win32Exception((int) ret, ret.ToString());
        }
    }

    [DllImport("Mpr.dll")]
    private static extern int WNetUseConnection(
        IntPtr hwndOwner,
        NETRESOURCE lpNetResource,
        string lpPassword,
        string lpUserID,
        int dwFlags,
        string lpAccessName,
        string lpBufferSize,
        string lpResult
        );

    [DllImport("Mpr.dll")]
    private static extern int WNetCancelConnection2(
        string lpName,
        int dwFlags,
        bool fForce
        );

    [StructLayout(LayoutKind.Sequential)]
    private class NETRESOURCE
    {
        public int dwScope = 0;
        public int dwType = 0;
        public int dwDisplayType = 0;
        public int dwUsage = 0;
        public string lpLocalName = "";
        public string lpRemoteName = "";
        public string lpComment = "";
        public string lpProvider = "";
    }

    private static void ConnectToRemote(string remoteUNC, string username, string password, bool promptUser)
    {
        NETRESOURCE nr = new NETRESOURCE
        {
            dwType = RESOURCETYPE_DISK,
            lpRemoteName = remoteUNC
        };

        NetworkError ret;
        if (promptUser)
            ret = (NetworkError) WNetUseConnection(IntPtr.Zero, nr, "", "", CONNECT_INTERACTIVE | CONNECT_PROMPT, null, null, null);
        else
            ret = (NetworkError) WNetUseConnection(IntPtr.Zero, nr, password, username, 0, null, null, null);

        if (ret != NetworkError.NO_ERROR)
        {
            throw new Win32Exception((int) ret, ret.ToString());
        }
    }
}

Show source
| c#   | .net   | garbage-collection   | using   | idisposable   2016-07-26 15:07 2 Answers

Answers to GC.KeepAlive to preserve a context ( 2 )

  1. 2016-07-26 16:07

    It is pretty easy to test, here is a quick test program, be sure it is run in release mode without a debugger attached.

    using System;
    
    namespace SandboxConsole
    { 
        class Program
        {
            static void Main(string[] args)
            {
                using (var context = new TestClass())
                {
    
                    GC.Collect();
                    GC.WaitForPendingFinalizers();
                    GC.Collect();
    
                    Console.WriteLine("After collection");
                }
                Console.WriteLine("After dispose, before 2nd collection");
    
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
    
                Console.WriteLine("After 2nd collection");
                Console.ReadLine();
            }
        }
    
        internal class TestClass : IDisposable
        {
            public void Dispose()
            {
                Dispose(true);
            }
    
            ~TestClass()
            {
                Console.WriteLine("In finalizer");
                Dispose(false);
            }
    
            private void Dispose(bool isDisposing)
            {
                Console.WriteLine("In Dispose: {0}", isDisposing);
                if (isDisposing)
                {
                    //uncomment this line out to have the finalizer never run
                    //GC.SuppressFinalize(this);
                }
            }
        }
    }
    

    It will always output

    After collection
    In Dispose: True
    After dispose, before 2nd collection
    In finalizer
    In Dispose: False
    After 2nd collection
    

    For more concrete proof, here is the IL for the above program's Main method

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // Code size       85 (0x55)
      .maxstack  1
      .locals init ([0] class SandboxConsole.TestClass context)
      IL_0000:  newobj     instance void SandboxConsole.TestClass::.ctor()
      IL_0005:  stloc.0
      .try
      {
        IL_0006:  call       void [mscorlib]System.GC::Collect()
        IL_000b:  call       void [mscorlib]System.GC::WaitForPendingFinalizers()
        IL_0010:  call       void [mscorlib]System.GC::Collect()
        IL_0015:  ldstr      "After collection"
        IL_001a:  call       void [mscorlib]System.Console::WriteLine(string)
        IL_001f:  leave.s    IL_002b
      }  // end .try
      finally
      {
        IL_0021:  ldloc.0
        IL_0022:  brfalse.s  IL_002a
        IL_0024:  ldloc.0
        IL_0025:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
        IL_002a:  endfinally
      }  // end handler
      IL_002b:  ldstr      "After dispose, before 2nd collection"
      IL_0030:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_0035:  call       void [mscorlib]System.GC::Collect()
      IL_003a:  call       void [mscorlib]System.GC::WaitForPendingFinalizers()
      IL_003f:  call       void [mscorlib]System.GC::Collect()
      IL_0044:  ldstr      "After 2nd collection"
      IL_0049:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_004e:  call       string [mscorlib]System.Console::ReadLine()
      IL_0053:  pop
      IL_0054:  ret
    } // end of method Program::Main
    

    You can see there is a hidden finally block that checks if the object is null then calls Dispose on it. That reference will keep the object alive the entire scope of the using block.

    UPDATE: See Damien's comment below, this specific example does have the opportunity to actually call the finalizer early due to the fact I don't ever use any variables that use a implicit this in the dispose method. To guarantee the behavior be sure to use a instance level variable (which my short example has none) or have GC.SuppressFinalize(this); uncommented.

  2. 2016-07-26 16:07

    The GC.KeepAlive method is empty. All it does is ensure that a particular variable is read from at that point in the code, because otherwise that variable is never read from again and is thus not a valid reference to keep an object alive.

    It's pointless here because the same variable that you're passing to KeepAlive is read from again at a later point in time - during the hidden finally block when Dispose is called. So, the GC.KeepAlive achieves nothing here.

Leave a reply to - GC.KeepAlive to preserve a context

◀ Go back