使用从未发生的`FirstAsync`观察器任务避免资源泄漏

时间:2018-03-14 16:03:14

标签: c# .net .net-4.5 system.reactive

我有一些软件带有基于事件的控制网络协议,它使用IObservable<Event>来处理绑定的消息。

在许多情况下,发送的消息将指示特定的响应(或序列,例如报告进度)。为了不会错过响应,正在使用FirstAsyncToTask预先设置任务,但是如果任务永远不会完成,这些似乎会泄漏。

也不允许简单地将evtTask放在using块中,因为不允许处理不完整的任务。

var jobUuid = Guid.NewGuid();
var evtTask = Events.FirstAsync((x) => x.Action == Action.JobComplete && x.JobUuid == jobUuid).ToTask();
// e.g. if this throws without ever sending the message
await SendMessage($"job {jobUuid} download {url}");
var evt = await evtTask;
if (evt.Success)
{
    ...
}

图书馆是否为此用例提供了一种简单的方法,可以在退出范围时取消订阅?

var jobUuid = Guid.NewGuid();
using(var evtTask = Events.FirstAsync((x) => x.Action == Action.JobComplete && x.JobUuid == jobUuid)
    .ToDisposableTask())) // Some method like this
{
    // e.g. if this throws without ever sending the message
    await SendMessage($"job {jobUuid} download {url}");
    var evt = await evtTask;
    if (evt.Success)
    {
        ...
    }
} // Get rid of the FirstAsync task if leave here before it completes for any reason

2 个答案:

答案 0 :(得分:2)

处置Task无济于事,因为它没有任何用处(在大多数情况下,包括这个)。什么会帮助取消任务。取消处置由ToTask创建的基础订阅等,解决了这个&#34;泄漏&#34;。

所以它可以这样:

Task<Event> evtTask;
using (var cts = new CancellationTokenSource()) {
    evtTask = Events.FirstAsync((x) => x.Action == Action.JobComplete && x.JobUuid == jobUuid)
             .ToTask(cts.Token);
    // e.g. if this throws without ever sending the message
    try {
        await SendMessage($"job {jobUuid} download {url}");
    }
    catch {
        cts.Cancel(); // disposes subscription
        throw;
    }
}
var evt = await evtTask;
if (evt.Success)
{
    ...
}

当然你可以用一些更方便的形式(比如扩展方法)来包装它。例如:

public static class ObservableExtensions {
    public static CancellableTaskWrapper<T> ToCancellableTask<T>(this IObservable<T> source) {
        return new CancellableTaskWrapper<T>(source);
    }

    public class CancellableTaskWrapper<T> : IDisposable
    {
        private readonly CancellationTokenSource _cts;
        public CancellableTaskWrapper(IObservable<T> source)
        {
            _cts = new CancellationTokenSource();
            Task = source.ToTask(_cts.Token);
        }

        public Task<T> Task { get; }

        public void Dispose()
        {
            _cts.Cancel();
            _cts.Dispose();
        }
    }
}

然后它变得接近你想要的东西:

var jobUuid = Guid.NewGuid();
using (var evtTask = Events.FirstAsync((x) => x.Action == Action.JobComplete && x.JobUuid == jobUuid).ToCancellableTask()) {
    await SendMessage($"job {jobUuid} download {url}");
    var evt = await evtTask.Task;
    if (evt.Success) {
        ...
    }
}

答案 1 :(得分:1)

您可以使用TPL Timeout(由@Fabjan引用)或Rx / System.Reactive版本的Timeout。

using听起来不错,但没有意义。使用相当于在使用块末尾的某些内容上调用.Dispose。我假设这里的问题是你的代码永远不会超过await evtTask。在假设的using中抛出所有内容不会改变任何内容:您的代码仍在等待。

在更高级别,您的代码比被动代码更重要,您可能希望将其重构为:

var subscription = Events
    .Where(x => x.Action == Action.JobComplete)
    .Subscribe(x => 
    {
        if(x.Success)
        {
            //...
        }
        else
        {
            //...
        }
    });