【问题标题】:Cancelling a Task when an object is Finalized完成对象时取消任务
【发布时间】:2016-04-06 09:48:41
【问题描述】:

我有一个启动任务的类,并希望确保在对象被垃圾收集时任务停止。

我已经实现了 IDisposable 模式,以确保如果手动释放对象或在 using 块中使用对象,则任务会正确停止。 但是,我不能保证最终用户会调用 Dispose() 或在 using 块中使用该对象。我知道垃圾收集器最终会调用终结器——这是否意味着任务仍在运行?

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);
    }
}

this question 中建议添加一个 volatile bool,它是从 Finalizer 设置并从任务中观察到的。这是推荐的,还是有更好的方法来实现我的需要?

(我使用的是 .NET 4,因此使用 TaskFactory.StartNew 而不是 Task.Run)

编辑:

为了给这个问题提供一些上下文 - 这实际上并没有在上面的代码 sn-p 中显示:我正在创建一个网络客户端类,它具有通过定期向服务器发送数据包来保持活动状态的机制。我选择不将所有这些细节都放在示例中,因为它与我的具体问题无关。但是,我真正想要的是用户能够将 KeepAlive 布尔属性设置为 true,这将启动每 60 秒向服务器发送数据的任务。如果用户将该属性设置为 false,则任务将停止。 IDisposable 让我完成了 90% 的工作,但它依赖于用户正确处理它(明确地或通过使用)。我不想向用户公开保持活动任务让他们明确取消,我只想要一个“简单”的 KeepAlive = true/false 来启动/停止任务,并且我希望任务在用户完成时停止对象 - 即使他们没有正确处理它。我开始认为这是不可能的!

【问题讨论】:

  • 为什么不使用System.Timers.Timer
  • 这段代码在终结器中不会做任何事情。那是一个错误。此外,由于任务通过其关闭状态保持在其外部 MyClass 实例上,因此该对象将永远不会被最终确定,直到任务自然退出。
  • @Danny Chen 我考虑过 - 但这有同样的问题吗?据我了解,即使创建它的对象已被处置,计时器也会继续运行(如果我错了,请纠正我)!我曾考虑将 AutoReset 设置为 false 并让回调方法在每次“滴答”时手动重置它 - 但随后不确定如果 GC 订阅了事件,它是否会最终确定对象。任何建议将不胜感激!
  • Dispose() 是一把锤子,它可以敲打太多钉子。这里只是大错特错终结器没用,任务正在使用对象。调用 Dispose() 只是在尝试使用已处理的对象时使任务随机失败的好方法。取消任务不是即时的。你必须停止使用 Dispose,它的合约根本无法完成工作。
  • @phil_rawlings 为什么要存储任务?为什么将其包装在自定义类中?如果要取消任务,请传递从用户控制的 CTS 创建的 CancellationToken。用户应直接调用 Cancel(),而不是通过阻塞 Dispose 间接调用

标签: c# task-parallel-library task idisposable finalizer


【解决方案1】:

我会草拟一个答案。我不是 100% 相信这会奏效。定稿是一个复杂的问题,我并不精通。

  1. 任务中不能有任何对象引用到任何应该完成的对象。
  2. 您不能从已知不安全的终结器中触摸其他对象。内置的 .NET 类通常不会记录此安全属性。你不能依赖它(通常)。
class CancellationFlag { public volatile bool IsSet; }

您现在可以在任务和MyClass 之间共享此类的实例。任务必须轮询标志并且MyClass 必须设置它。

为了确保任务不会意外引用外部对象,我将编写如下代码:

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

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

通过这种方式,您可以通过state 显式线程化任何状态。这至少是CancellationFlag 的一个实例,但在任何情况下都不会引用MyClass

【讨论】:

  • 我会做类似的事情,除了我将 volatile bool 标志直接放在消费类中,因为这样就不需要考虑CancellationFlag 是否安全(即使在这种情况下是微不足道的)。
  • 消费类是什么意思?他不能将它放在 MyClass 中,因为这会阻止最终确定。如果您的意思是他可能需要为state 创建的课程,那么我同意。
  • 你是对的;我又错过了关闭。我认为这是最好的方法。
  • 似乎是迄今为止最好的建议 - 我将尝试对此进行测试。一个问题:当我们尝试在 MyClass Finalizer 中设置 CancellationFlag 时,是否有可能已经完成?在这种情况下,我认为这种方法可能行不通是对的?
  • CancellationFlag 没有终结器,因此它不会被终结。对象在最终确定时不会变得“无效”。所发生的一切就是 Finalize 被调用(一次)。不多不少。您的 CancellationFlag 和新的一样好。但也要注意 CancellationFlag 可以从任务中访问。
【解决方案2】:

我创建了下面的程序来探索差异......

根据我对它的观察,无论是取消令牌还是 volatile bool 似乎都没有区别,真正重要的是 Task.StartNew 方法不是使用 lambda 表达式调用的。

编辑: 澄清:如果 lambda 引用静态方法,那实际上没问题:当 lambda 导致包含对包含类的引用时出现问题:所以要么引用父类的成员变量或对父类实例方法的引用。

请尝试一下,如果您得出相同的结论,请告诉我。

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);
            }
        }
    }
}

更新:

添加了更多测试(现在包括在上面):并得出与“usr”相同的结论:如果有对父类的引用,则永远不会调用终结器(这是有道理的:存在活动引用,因此GC 没有启动)

【讨论】:

  • lambda 会导致非终结问题,所以这个测试表明了这一点。请注意,无法从终结器(您没有这样做)访问令牌。所以这种可能性不存在了。
  • 确实,它似乎支持你的回答@usr。我正在尝试其他一些排列,稍后会更新。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-10
  • 2021-09-06
  • 2015-07-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多