如何清理 C# 任务 API 上的挂起任务
本文关键字:任务 挂起 API 何清理 | 更新日期: 2023-09-27 18:36:24
我有一个简单的函数如下:
static Task<A> Peirce<A, B>(Func<Func<A, Task<B>>, Task<A>> a)
{
var aa = new TaskCompletionSource<A>();
var tt = new Task<A>(() =>
a(b =>
{
aa.SetResult(b);
return new TaskCompletionSource<B>().Task;
}).Result
);
tt.Start();
return Task.WhenAny(aa.Task, tt).Result;
}
这个想法很简单:对于a
的任何实现,它必须向我返回一个Task<A>
。为此,它可能会也可能不使用参数(类型 Func<A, Task<B>
)。如果是这样,我们的回调将被调用并设置aa
的结果,然后aa.Task
将完成。否则,a
的结果将不依赖于其参数,因此我们只需返回其值。在任何情况下,aa.Task
或a
的结果都将完成,因此除非 do 不使用其参数和块,或者由 a
块返回的任务,否则它不应阻塞。
上面的代码有效,例如
static void Main(string[] args)
{
Func<Func<int, Task<int>>, Task<int>> t = a =>
{
return Task.FromResult(a(20).Result + 10);
};
Console.WriteLine(Peirce(t).Result); // output 20
t = a => Task.FromResult(10);
Console.WriteLine(Peirce(t).Result); // output 10
}
这里的问题是,两个任务aa.Task
和tt
一旦确定了WhenAny
的结果就必须清理干净,否则恐怕会有挂任务的漏水。我不知道该怎么做,任何人都可以提出一些建议吗?或者这实际上不是问题,C# 会为我做吗?
附言这个名字Peirce
来自命题逻辑中著名的"皮尔斯定律"(((A->B)->A)->A
)。
更新:问题的重点不是"处置"任务,而是阻止它们运行。我已经测试过,当我将"主"逻辑放在 1000 个循环中时,它运行缓慢(大约 1 个循环/秒),并创建大量线程,因此这是一个需要解决的问题。
Task
是托管对象。 除非引入非托管资源,否则不必担心Task
泄漏资源。 让 GC 清理它,让终结器处理WaitHandle
。
编辑:
如果要取消任务,请考虑以CancellationTokenSource
的形式使用合作取消。 您可以通过重载将此令牌传递给任何任务,在每个任务中,您可能有一些代码,如下所示:
while (someCondition)
{
if (cancelToken.IsCancellationRequested)
break;
}
这样,您的任务就可以正常清理而不会引发异常。 但是,如果您调用cancelToken.ThrowIfCancellationRequested()
,则可以传播OperationCancelledException
。 因此,在您的情况下,您的想法是,首先完成的任何内容都可以向其他任务发出取消,这样它们就不会挂断工作。
感谢@Bryan Crosby的回答,我现在可以实现如下函数:
private class CanceledTaskCache<A>
{
public static Task<A> Instance;
}
private static Task<A> GetCanceledTask<A>()
{
if (CanceledTaskCache<A>.Instance == null)
{
var aa = new TaskCompletionSource<A>();
aa.SetCanceled();
CanceledTaskCache<A>.Instance = aa.Task;
}
return CanceledTaskCache<A>.Instance;
}
static Task<A> Peirce<A, B>(Func<Func<A, Task<B>>, Task<A>> a)
{
var aa = new TaskCompletionSource<A>();
Func<A, Task<B>> cb = b =>
{
aa.SetResult(b);
return GetCanceledTask<B>();
};
return Task.WhenAny(aa.Task, a(cb)).Unwrap();
}
而且效果很好:
static void Main(string[] args)
{
for (int i = 0; i < 1000; ++i)
{
Func<Func<int, Task<String>>, Task<int>> t =
async a => (await a(20)).Length + 10;
Console.WriteLine(Peirce(t).Result); // output 20
t = async a => 10;
Console.WriteLine(Peirce(t).Result); // output 10
}
}
现在它速度很快,不会消耗太多资源。 如果您不使用 async/await 关键字,它可以更快(在我的机器中大约 70 次):
static void Main(string[] args)
{
for (int i = 0; i < 10000; ++i)
{
Func<Func<int, Task<String>>, Task<int>> t =
a => a(20).ContinueWith(ta =>
ta.IsCanceled ? GetCanceledTask<int>() :
Task.FromResult(ta.Result.Length + 10)).Unwrap();
Console.WriteLine(Peirce(t).Result); // output 20
t = a => Task.FromResult(10);
Console.WriteLine(Peirce(t).Result); // output 10
}
}
问题是,即使你可以检测到a(20)
的返回值,也没有办法取消async
块而不是抛出OperationCanceledException
,它会阻止WhenAny
被优化。
更新:优化的代码,并比较了异步/等待和本机任务API。
更新:如果我能编写以下代码,那将是理想的:
static Task<A> Peirce<A, B>(Func<Func<A, Task<B>>, Task<A>> a)
{
var aa = new TaskCompletionSource<A>();
return await? a(async b => {
aa.SetResult(b);
await break;
}) : await aa.Task;
}
在这里,如果 a 成功,await? a : b
具有值a
的结果,如果 a 被取消,则具有值 b(如 a ? b : c
,a
的结果的值应具有相同类型的 b)。 await break
将取消当前的异步块。
正如MS并行编程团队的Stephen Toub所说:"不。不要费心处理你的任务。
tldr:在大多数情况下,释放任务不会执行任何操作,当任务实际分配了非托管资源时,其终结器将在收集任务对象时释放这些资源。