在我的.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实现目标的方法。
答案 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