在对象完成时取消任务
Cancelling a Task when an object is Finalized
我有一个 class 可以启动一个任务,并希望确保该任务在对象被垃圾回收时停止。
我已经实现了 IDisposable 模式,以确保如果对象是手动释放的或在 using 块中使用的,则 Task 会正确停止。 但是,我不能保证最终用户会调用 Dispose() 或在 using 块中使用该对象。我知道 Garbage Collector 最终会调用 Finalizer - 这是否意味着任务被留下 运行?
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 中建议添加一个从终结器设置并从任务观察到的可变布尔值。这是推荐的,还是有更好的方法来实现我所需要的?
(我使用的是 .NET 4,因此使用 TaskFactory.StartNew 而不是 Task.Run)
编辑:
为了给这个问题提供一些上下文——上面的代码片段中实际上没有显示:我正在创建一个网络客户端 class,它有一种通过定期向服务器发送数据包来保持活动的机制。我选择不将所有这些细节都放在示例中,因为它与我的具体问题无关。然而,我真正想要的是用户能够将 KeepAlive 布尔值 属性 设置为 true,这将启动一个任务,每 60 秒向服务器发送一次数据。如果用户将 属性 设置为 false,则任务停止。 IDisposable 让我完成了 90% 的工作,但是它依赖于用户正确地处理它(明确地或通过使用)。我不想向用户公开保持活动任务以便他们明确取消,我只想要 "simple" KeepAlive = true/false 到 start/stop 任务并且我希望任务停止当用户使用完该对象时——即使他们没有正确处理它。我开始认为这是不可能的!
我会草拟一个答案。我不是 100% 相信这会奏效。 finalization是个复杂的问题,我也不是很精通
- 不能有从任务到任何应该完成的对象的对象引用。
- 您不能从终结器中触摸未知安全的其他对象。内置的 .NET classes 通常不会记录这种安全性 属性。你不能依赖那个(通常)。
class CancellationFlag { public volatile bool IsSet; }
您现在可以在任务和 MyClass
之间共享此 class 的一个实例。任务必须轮询标志并且 MyClass
必须设置它。
为了确保任务永远不会意外引用外部对象,我将代码结构如下:
Task.Factory.StartNew(TaskProc, state); //no lambda
static void TaskProc(object state) { //static
}
通过这种方式,您可以通过 state
显式线程化任何状态。这至少是 CancellationFlag
的一个实例,但在任何情况下都不会引用 MyClass
.
我创建了下面的程序来探索差异...
根据我对它的观察,它是取消标记还是 volatile bool 似乎没有区别,真正重要的是 Task.StartNew 方法不是使用 lambda 表达式调用的。
编辑: 澄清:如果 lambda 引用静态方法,实际上没问题:当 lambda 导致对包含 class 的引用时,问题就来了要包含:因此要么引用父 class 的成员变量,要么引用父 class.
的实例方法
请试一试,如果您得出相同的结论,请告诉我。
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" 相同的结论:如果存在对父 class 的引用,则永远不会调用终结器(这是有道理的: 存在一个活动引用,因此 GC 不会启动)
我有一个 class 可以启动一个任务,并希望确保该任务在对象被垃圾回收时停止。
我已经实现了 IDisposable 模式,以确保如果对象是手动释放的或在 using 块中使用的,则 Task 会正确停止。 但是,我不能保证最终用户会调用 Dispose() 或在 using 块中使用该对象。我知道 Garbage Collector 最终会调用 Finalizer - 这是否意味着任务被留下 运行?
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 中建议添加一个从终结器设置并从任务观察到的可变布尔值。这是推荐的,还是有更好的方法来实现我所需要的?
(我使用的是 .NET 4,因此使用 TaskFactory.StartNew 而不是 Task.Run)
编辑:
为了给这个问题提供一些上下文——上面的代码片段中实际上没有显示:我正在创建一个网络客户端 class,它有一种通过定期向服务器发送数据包来保持活动的机制。我选择不将所有这些细节都放在示例中,因为它与我的具体问题无关。然而,我真正想要的是用户能够将 KeepAlive 布尔值 属性 设置为 true,这将启动一个任务,每 60 秒向服务器发送一次数据。如果用户将 属性 设置为 false,则任务停止。 IDisposable 让我完成了 90% 的工作,但是它依赖于用户正确地处理它(明确地或通过使用)。我不想向用户公开保持活动任务以便他们明确取消,我只想要 "simple" KeepAlive = true/false 到 start/stop 任务并且我希望任务停止当用户使用完该对象时——即使他们没有正确处理它。我开始认为这是不可能的!
我会草拟一个答案。我不是 100% 相信这会奏效。 finalization是个复杂的问题,我也不是很精通
- 不能有从任务到任何应该完成的对象的对象引用。
- 您不能从终结器中触摸未知安全的其他对象。内置的 .NET classes 通常不会记录这种安全性 属性。你不能依赖那个(通常)。
class CancellationFlag { public volatile bool IsSet; }
您现在可以在任务和 MyClass
之间共享此 class 的一个实例。任务必须轮询标志并且 MyClass
必须设置它。
为了确保任务永远不会意外引用外部对象,我将代码结构如下:
Task.Factory.StartNew(TaskProc, state); //no lambda
static void TaskProc(object state) { //static
}
通过这种方式,您可以通过 state
显式线程化任何状态。这至少是 CancellationFlag
的一个实例,但在任何情况下都不会引用 MyClass
.
我创建了下面的程序来探索差异...
根据我对它的观察,它是取消标记还是 volatile bool 似乎没有区别,真正重要的是 Task.StartNew 方法不是使用 lambda 表达式调用的。
编辑: 澄清:如果 lambda 引用静态方法,实际上没问题:当 lambda 导致对包含 class 的引用时,问题就来了要包含:因此要么引用父 class 的成员变量,要么引用父 class.
的实例方法请试一试,如果您得出相同的结论,请告诉我。
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" 相同的结论:如果存在对父 class 的引用,则永远不会调用终结器(这是有道理的: 存在一个活动引用,因此 GC 不会启动)