用Observable替换TaskCompletionSource

时间:2014-04-02 22:12:12

标签: c# system.reactive taskcompletionsource

在我的.NET 4.0库中,我有一段代码通过网络发送数据并等待响应。为了不阻止调用代码,该方法返回一个Task<T>,它在收到响应时完成,以便代码可以调用这样的方法:

// Send the 'message' to the given 'endpoint' and then wait for the response
Task<IResult> task = sender.SendMessageAndWaitForResponse(endpoint, message);
task.ContinueWith(
    t => 
    {
        // Do something with t.Result ...
    });

底层代码使用TaskCompletionSource,以便它可以等待响应消息而不必启动一个线程,只是让它在空闲状态直到响应进入:

private readonly Dictionary<int, TaskCompletionSource<IResult>> m_TaskSources
    = new Dictionary<int, TaskCompletionSource<IResult>>();

public Task<IResult> SendMessageAndWaitForResponse(int endpoint, object message)
{
    var source = new TaskCompletionSource<IResult>(TaskCreationOptions.None);
    m_TaskSources.Add(endpoint, source);

    // Send the message here ...

    return source.Task;
}

收到回复时,会按以下方式处理:

public void CompleteWaitForResponseResponse(int endpoint, IResult value)
{
    if (m_TaskSources.ContainsKey(endpoint))
    {
        var source = m_TaskSources[endpoint];
        source.SetResult(value);
        m_TaskSources.Remove(endpoint);
    }
}

现在我想添加一个超时,以便调用代码不会无限期地等待响应。但是在.NET 4.0上somewhat messy因为没有简单的方法来超时任务。所以我想知道Rx是否能够更容易地做到这一点。所以我想出了以下内容:

private readonly Dictionary<int, Subject<IResult>> m_SubjectSources
    = new Dictionary<int, Subject<IResult>>();

private Task<IResult> SendMessageAndWaitForResponse(int endpoint, object message, TimeSpan timeout)
{
    var source = new Subject<IResult>();
    m_SubjectSources.Add(endpoint, source);

    // Send the message here ...

    return source.Timeout(timeout).ToTask();
}

public void CompleteWaitForResponseResponse(int endpoint, IResult value)
{
    if (m_SubjectSources.ContainsKey(endpoint))
    {
        var source = m_SubjectSources[endpoint];
        source.OnNext(value);
        source.OnCompleted();
        m_SubjectSources.Remove(endpoint);
    }
}

这一切似乎都没有问题,但我已经看到几个问题表明Subject应该是avoided所以现在我想知道是否还有更多的Rx-y实现目标的方法。

1 个答案:

答案 0 :(得分:1)

避免在Rx中使用Subject的建议经常被夸大。 Rx中必须有一个事件来源,它可以成为Subject

主题的问题通常是在两个可以加入的Rx查询之间使用,或者已经有明确定义的IObservable<T>转换(例如Observable.FromEventXXX或{ {1}}等等。

如果您愿意,可以使用以下方法取消Observable.FromAsyncXXX和多个Dictionary。这使用单个主题并将筛选的查询返回给客户端。

这不是“更好”本身,这是否有意义取决于您的方案的具体情况,但它可以节省大量的主题,并为您提供监控所有结果的不错选择单个流。如果您按顺序(例如从消息队列)调度结果,这可能是有意义的。

Subject

我为这样的结果定义了一个类:

// you only need to synchronize if you are receiving results in parallel
private readonly ISubject<Tuple<int,IResult>, Tuple<int,IResult>> results =
    Subject.Synchronize(new Subject<Tuple<int,IResult>>());

private Task<IResult> SendMessageAndWaitForResponse(
    int endpoint, object message, TimeSpan timeout)
{           
    // your message processing here, I'm just echoing a second later
    Task.Delay(TimeSpan.FromSeconds(1)).ContinueWith(t => {
        CompleteWaitForResponseResponse(endpoint, new Result { Value = message }); 
    });

    return results.Where(r => r.Item1 == endpoint)
                  .Select(r => r.Item2)
                  .Take(1)
                  .Timeout(timeout)
                  .ToTask();
}

public void CompleteWaitForResponseResponse(int endpoint, IResult value)
{
    results.OnNext(Tuple.Create(endpoint,value));
}

编辑 - 回应评论中的其他问题。

  • 无需处理单个主题 - 它不会泄漏,当它超出范围时会被垃圾收集。

  • public class Result : IResult { public object Value { get; set; } } public interface IResult { object Value { get; set; } } 确实接受取消令牌 - 但这实际上是从客户端取消。

  • 如果远程端断开连接,您可以将错误发送给ToTask的所有客户端 - 您将要同时实例化新的主题实例。

类似的东西:

results.OnError(exception);

这将以预期的方式向所有客户表现出错误的任务。

这也非常安全,因为订阅以前发送过private void OnRemoteError(Exception e) { results.OnError(e); } 主题的客户会立即收到错误 - 从那时起就已经死了。然后准备就绪时,您可以重新初始化:

OnError

对于个别客户端错误,您可以考虑:

  • 扩展您的private void OnInitialiseConnection() { // ... your connection logic // reinitialise the subject... results = Subject.Synchronize(new Subject<Tuple<int,IResult>>()); } 界面以包含数据错误
  • 然后,您可以通过在IResult中扩展Rx查询,选择性地将此项目预测为该客户的故障。例如,以及IResult的Exception和HasError属性,以便您可以执行以下操作:

    SendMessageAndWaitForResponse