Cancelling a Task when an object is Finalized

2019-04-12 00:09发布

问题:

I have a class which starts a Task and want to ensure that the Task stops when the object is garbage collected.

I have implemented the IDisposable pattern to ensure that if the object is disposed manually or used within a using block, then the Task stops correctly. However, I cant guarantee that the end user will call Dispose() or use the object within a using block. I know that the Garbage Collector will eventually call the Finalizer - does this mean that the task is left running?

public class MyClass : IDisposable
{
    private readonly CancellationTokenSource feedCancellationTokenSource = 
          new CancellationTokenSource();

    private readonly Task feedTask;

    public MyClass()
    {
        feedTask = Task.Factory.StartNew(() =>
        {
            while (!feedCancellationTokenSource.IsCancellationRequested)
            {
                // do finite work
            }
        });
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            feedCancellationTokenSource.Cancel();
            feedTask.Wait();

            feedCancellationTokenSource.Dispose();
            feedTask.Dispose();
        }
    }

    ~MyClass()
    {
        Dispose(false);
    }
}

It was suggested in this question to add a volatile bool which is set from the Finalizer and observed from the task. Is this recommended, or is there a better way to achieve what I need?

(I'm using .NET 4 hence the use of TaskFactory.StartNew rather than Task.Run)

EDIT:

To give some context to the question - which is not actually shown in the above code snippet: I am creating a Network client class which has a mechanism to keep alive by regularly sending packets to the server. I chose not to put all this detail in the example as it wasn't relevant to my specific question. However, what I actually want is the ability for the user to set a KeepAlive boolean property to true, which will start a task to send data to the server every 60 seconds. If the user sets the property to false then the task stops. IDisposable got me 90% of the way there, however it relies on the user disposing it properly (explicitly or via using). I don't want to expose keep alive tasks to the user for them to cancel explicitly, I just want a "simple" KeepAlive = true/false to start/stop the task AND I want the task to stop when the user is finished with the object - even if they don't dispose of it properly. I'm starting to think that this isn't possible!

回答1:

I'll sketch an answer. I'm not 100% confident that this will work. Finalization is a complicated issue and I'm not proficient in it.

  1. There can be no object reference from the task to whatever object is supposed to be finalized.
  2. You can't touch other objects from a finalizer that are not known to be safe. The built-in .NET classes do not usually document this safety property. You can't rely on that (usually).
class CancellationFlag { public volatile bool IsSet; }

You can now share an instance of this class between the task and MyClass. The task must poll the flag and MyClass must set it.

In order to ensure that the task never accidentally references the outer object I'd structure the code like this:

Task.Factory.StartNew(TaskProc, state); //no lambda

static void TaskProc(object state) { //static
}

This way you can explicitly thread any state through state. This would, at least, be an instance of CancellationFlag but under no circumstances a reference to MyClass.



回答2:

I created the program below to explore the differences...

From my observations with it, it looks like it makes no difference whether it's a cancellation token or a volatile bool, what really matters is that the Task.StartNew method isn't called using a lambda expression.

Edit: to clarify: if the lambda refers to a static method, it's actually fine: the problem comes when the lambda causes a reference to the containing class to be included: so either a reference to a member variable of the parent class or else a reference to an instance method of the parent class.

Please do give this a try and let me know if you come to the same conclusion.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication7
{
    class Program
    {
        static void Main(string[] args)
        {
            Logger.LogFile = @"c:\temp\test\log.txt";

            Task.Run(() =>
            {
                // two instances (not disposed properly)

                // if left to run, this background task keeps running until the application exits
                var c1 = new MyClassWithVolatileBoolCancellationFlag();

                // if left to run, this background task cancels correctly
                var c2 = new MyClassWithCancellationSourceAndNoLambda();

                //
                var c3 = new MyClassWithCancellationSourceAndUsingTaskDotRun();

                //
                var c4 = new MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference();


            }).GetAwaiter().GetResult();

            // instances no longer referenced at this point

            Logger.Log("Press Enter to exit");
            Console.ReadLine(); // press enter to allow the console app to exit normally: finalizer gets called on both instances
        }


        static class Logger
        {
            private static object LogLock = new object();
            public static string LogFile;
            public static void Log(string toLog)
            {
                try
                {
                    lock (LogLock)
                        using (var f = File.AppendText(LogFile))
                            f.WriteLine(toLog);

                    Console.WriteLine(toLog);
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Logging Exception: " + ex.ToString());
                }
            }

        }

        // finalizer gets called eventually  (unless parent process is terminated)
        public class MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference : IDisposable
        {
            private CancellationTokenSource cts = new CancellationTokenSource();

            private readonly Task feedTask;

            public MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference()
            {
                Logger.Log("New MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference Instance");

                var token = cts.Token; // NB: by extracting the struct here (instead of in the lambda in the next line), we avoid the parent reference (via the cts member variable)
                feedTask = Task.Run(() => Background(token)); // token is a struct
            }

            private static void Background(CancellationToken token)  // must be static or else a reference to the parent class is passed
            {
                int i = 0;
                while (!token.IsCancellationRequested) // reference to cts means this class never gets finalized
                {
                    Logger.Log("Background task for MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference running. " + i++);
                    Thread.Sleep(1000);
                }
            }

            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            protected virtual void Dispose(bool disposing)
            {
                cts.Cancel();

                if (disposing)
                {
                    feedTask.Wait();

                    feedTask.Dispose();

                    Logger.Log("MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference Disposed");
                }
                else
                {
                    Logger.Log("MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference Finalized");
                }
            }

            ~MyClassWithCancellationSourceAndUsingTaskDotRunButNoParentReference()
            {
                Dispose(false);
            }
        }

        // finalizer doesn't get called until the app is exiting: background process keeps running
        public class MyClassWithCancellationSourceAndUsingTaskDotRun : IDisposable
        {
            private CancellationTokenSource cts = new CancellationTokenSource();

            private readonly Task feedTask;

            public MyClassWithCancellationSourceAndUsingTaskDotRun()
            {
                Logger.Log("New MyClassWithCancellationSourceAndUsingTaskDotRun Instance");
                //feedTask = Task.Factory.StartNew(Background, cts.Token);
                feedTask = Task.Run(() => Background());
            }

            private void Background()
            {
                    int i = 0;
                    while (!cts.IsCancellationRequested) // reference to cts & not being static means this class never gets finalized
                    {
                        Logger.Log("Background task for MyClassWithCancellationSourceAndUsingTaskDotRun running. " + i++);
                        Thread.Sleep(1000);
                    }
            }

            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            protected virtual void Dispose(bool disposing)
            {
                cts.Cancel();

                if (disposing)
                {
                    feedTask.Wait();

                    feedTask.Dispose();

                    Logger.Log("MyClassWithCancellationSourceAndUsingTaskDotRun Disposed");
                }
                else
                {
                    Logger.Log("MyClassWithCancellationSourceAndUsingTaskDotRun Finalized");
                }
            }

            ~MyClassWithCancellationSourceAndUsingTaskDotRun()
            {
                Dispose(false);
            }
        }


        // finalizer gets called eventually  (unless parent process is terminated)
        public class MyClassWithCancellationSourceAndNoLambda : IDisposable
        {
            private CancellationTokenSource cts = new CancellationTokenSource();

            private readonly Task feedTask;

            public MyClassWithCancellationSourceAndNoLambda()
            {
                Logger.Log("New MyClassWithCancellationSourceAndNoLambda Instance");
                feedTask = Task.Factory.StartNew(Background, cts.Token);
            }

            private static void Background(object state)
            {
                var cancelled = (CancellationToken)state;
                if (cancelled != null)
                {
                    int i = 0;
                    while (!cancelled.IsCancellationRequested)
                    {
                        Logger.Log("Background task for MyClassWithCancellationSourceAndNoLambda running. " + i++);
                        Thread.Sleep(1000);
                    }
                }
            }

            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            protected virtual void Dispose(bool disposing)
            {
                cts.Cancel();

                if (disposing)
                {
                    feedTask.Wait();

                    feedTask.Dispose();

                    Logger.Log("MyClassWithCancellationSourceAndNoLambda Disposed");
                }
                else
                {
                    Logger.Log("MyClassWithCancellationSourceAndNoLambda Finalized");
                }
            }

            ~MyClassWithCancellationSourceAndNoLambda()
            {
                Dispose(false);
            }
        }


        // finalizer doesn't get called until the app is exiting: background process keeps running
        public class MyClassWithVolatileBoolCancellationFlag : IDisposable
        {
            class CancellationFlag { public volatile bool IsSet; }

            private CancellationFlag cf = new CancellationFlag();

            private readonly Task feedTask;

            public MyClassWithVolatileBoolCancellationFlag()
            {
                Logger.Log("New MyClassWithVolatileBoolCancellationFlag Instance");
                feedTask = Task.Factory.StartNew(() =>
                {
                    int i = 0;
                    while (!cf.IsSet)
                    {
                        Logger.Log("Background task for MyClassWithVolatileBoolCancellationFlag running. " + i++);
                        Thread.Sleep(1000);
                    }
                });
            }


            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            protected virtual void Dispose(bool disposing)
            {
                cf.IsSet = true;

                if (disposing)
                {
                    feedTask.Wait();

                    feedTask.Dispose();

                    Logger.Log("MyClassWithVolatileBoolCancellationFlag Disposed");
                }
                else
                {
                    Logger.Log("MyClassWithVolatileBoolCancellationFlag Finalized");
                }
            }

            ~MyClassWithVolatileBoolCancellationFlag()
            {
                Dispose(false);
            }
        }
    }
}

Update:

Added a few more tests (now included above): and came to the same conclusion as "usr": the finalizer never gets called if there's a reference to the parent class (which makes sense: an active reference exists, therefore the GC doesn't kick in)