C# 将TaskCompletionSource替换为Observable

C# 将TaskCompletionSource替换为Observable,c#,system.reactive,taskcompletionsource,C#,System.reactive,Taskcompletionsource,在我的.NET4.0库中,我有一段代码通过网络发送数据并等待响应。为了不阻止调用代码,该方法返回一个任务,该任务在收到响应时完成,以便代码可以像这样调用该方法: // Send the 'message' to the given 'endpoint' and then wait for the response Task<IResult> task = sender.SendMessageAndWaitForResponse(endpoint, message); task.Con

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

// 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 ...
    });
public void CompleteWaitForResponseResponse(int endpoint, IResult value)
{
    if (m_TaskSources.ContainsKey(endpoint))
    {
        var source = m_TaskSources[endpoint];
        source.SetResult(value);
        m_TaskSources.Remove(endpoint);
    }
}
public class Result : IResult
{
    public object Value { get; set; }
}

public interface IResult
{
    object Value { get; set; }
}
现在我想添加一个超时,这样调用代码就不会无限期地等待响应。然而,在.NET4.0上,这是因为没有简单的方法来超时任务。所以我想知道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);
    }
}
专用只读词典m_SubjectSources
=新字典();
私有任务SendMessageAndWaitForResponse(int端点、对象消息、TimeSpan超时)
{
var source=新主题();
m_SubjectSources.Add(端点,源);
//在这里发送消息。。。
返回source.Timeout(Timeout.ToTask();
}
public void CompleteWaitForResponseResponse(int端点,IResult值)
{
if(m_SubjectSources.ContainsKey(端点))
{
var source=m_SubjectSources[endpoint];
source.OnNext(值);
source.OnCompleted();
m_SubjectSources.Remove(端点);
}
}

这一切似乎都没有问题,但我已经看到一些问题表明
Subject
应该是这样,所以现在我想知道是否有一种更有效的方法来实现我的目标。

避免在Rx中使用
Subject
的建议往往被夸大了。Rx中必须有事件的源,并且可以将其作为
主题

Subject的问题通常是在两个Rx查询之间使用时,否则可能会被连接,或者已经有了到
IObservable
(例如
Observable.FromEventXXX
Observable.FromAsyncXXX
等)的明确转换

如果您愿意,您可以使用下面的方法取消
字典和多个
主题。这将使用单个主题并向客户端返回过滤后的查询

这本身并不是“更好”,这是否有意义取决于您的场景的具体情况,但它节省了大量的主题,并为您提供了一个很好的选项,用于监控单个流中的所有结果。如果您连续发送结果(例如从消息队列),这可能是有意义的

// 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));
}
编辑-回答评论中的其他问题

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

  • ToTask
    确实接受取消令牌-但这实际上是为了从客户端取消

  • 如果远程端断开连接,您可以向所有客户端发送一个包含
    results.OnError(exception);
    -您需要同时实例化一个新的主题实例

比如:

private void OnRemoteError(Exception e)
{
    results.OnError(e);        
}
return results.Where(r => r.Item1 == endpoint)
            .SelectMany(r => r.Item2.HasError
                ? Observable.Throw<IResult>(r.Item2.Exception)
                : Observable.Return(r.Item2))
            .Take(1)
            .Timeout(timeout)
            .ToTask();
这将以预期的方式向所有客户端显示为错误任务

这也是非常线程安全的,因为订阅以前发送了
OnError
的主题的客户端将立即返回一个错误-从那时起,它就死了。准备好后,您可以使用以下工具重新初始化:

private void OnInitialiseConnection()
{
    // ... your connection logic

    // reinitialise the subject...
    results = Subject.Synchronize(new Subject<Tuple<int,IResult>>());
}

你是用VS2010开发的吗?否则,你仍然可以针对.NET 4.0使用,它有
TaskEx.Delay
CancellationTokenSource.CancelAfter
等。不,我是在VS2012上。感谢指向BCL库的指针。我不知道那一个。不用担心,你可能还想读一下:重点是,
Task.Delay
在.NET 4.0中不可用。这就是我询问Microsoft.Bcl.Async
的原因。即使这不是一个选项,通过
System.Threading.Timer
实现仍然很简单,所以我想知道@Petrik的作用是什么。
任务。延迟
不是实现的一部分!!!这只是一个模拟响应的黑客程序。一旦你成功了
IObservable
,打开了任务无法使用的选项。您是否需要/想要它们是另一个问题。OP专门询问了Rx中的主题。我当然可以使用
System.Threading.Timer
或同等工具,但这会在我需要的基础上增加复杂性,我也不相信自己的技能所有这些线程/同步都很重要。在其他人已经为我解决了一些问题的地方,使用Rx或TPL似乎更好:)@JamesWorld感谢这个解决方案,它比我的方法干净多了。我有两个(有点相关)问题是如何处理任务取消,例如,如果远程端断开连接,我想取消该端所有未完成的任务。目前,我有一个
CancellationToken
,我用它来做这件事,但我不确定如何将它融入到您的方法中。第二,我是否必须处理任何东西,或者在任务完成时是否已处理?我假设我最终必须处理主ISubject实例?添加了关于个别客户端错误的更多想法。
return results.Where(r => r.Item1 == endpoint)
            .SelectMany(r => r.Item2.HasError
                ? Observable.Throw<IResult>(r.Item2.Exception)
                : Observable.Return(r.Item2))
            .Take(1)
            .Timeout(timeout)
            .ToTask();