C# 使用Rx在异步操作上阻塞(可能超时)
我正在尝试使用.NET的反应式扩展重写一些代码,但我需要一些关于如何实现我的目标的指导 我有一个类,它将一些异步行为封装在一个低级库中。想一想读或写网络的东西。当类启动时,它将尝试连接到环境,当成功时,它将通过从工作线程调用来发回信号 我想将这种异步行为转化为同步调用,并在下面创建了一个大大简化的示例,说明如何实现这一点:C# 使用Rx在异步操作上阻塞(可能超时),c#,.net,system.reactive,C#,.net,System.reactive,我正在尝试使用.NET的反应式扩展重写一些代码,但我需要一些关于如何实现我的目标的指导 我有一个类,它将一些异步行为封装在一个低级库中。想一想读或写网络的东西。当类启动时,它将尝试连接到环境,当成功时,它将通过从工作线程调用来发回信号 我想将这种异步行为转化为同步调用,并在下面创建了一个大大简化的示例,说明如何实现这一点: ManualResetEvent readyEvent = new ManualResetEvent(false); public void Start(TimeSpan
ManualResetEvent readyEvent = new ManualResetEvent(false);
public void Start(TimeSpan timeout) {
// Simulate a background process
ThreadPool.QueueUserWorkItem(_ => AsyncStart(TimeSpan.FromSeconds(1)));
// Wait for startup to complete.
if (!this.readyEvent.WaitOne(timeout))
throw new TimeoutException();
}
void AsyncStart(TimeSpan delay) {
Thread.Sleep(delay); // Simulate startup delay.
this.readyEvent.Set();
}
在工作线程上运行AsyncStart
,只是模拟库的异步行为的一种方式,而不是我真正的代码的一部分,低级库提供线程并在回调时调用我的代码
请注意,如果启动未在超时间隔内完成,则Start
方法将抛出TimeoutException
我想重写此代码以使用Rx。这是我的第一次尝试:
Subject<Unit> readySubject = new Subject<Unit>();
public void Start(TimeSpan timeout) {
ThreadPool.QueueUserWorkItem(_ => AsyncStart(TimeSpan.FromSeconds(1)));
// Point A - see below
this.readySubject.Timeout(timeout).First();
}
void AsyncStart(TimeSpan delay) {
Thread.Sleep(delay);
this.readySubject.OnNext(new Unit());
}
现在,异步操作不会立即启动,而是仅在使用IObservable
时启动。不幸的是,仍然存在争用条件,但这次是在B点。如果异步操作在延迟
lambda返回之前启动调用OnNext
,它仍然丢失,并且超时
将引发TimeoutException
我知道我可以使用像Replay
这样的操作符来缓冲事件,但是我最初的例子没有使用Rx,没有使用任何类型的缓冲。有没有一种方法可以让我在没有比赛条件的情况下使用Rx来解决我的问题?本质上,只有在IObservable
连接到后才能启动异步操作。在这种情况下,是超时
和第一次
根据Paul Betts的回答,这里是可行的解决方案:
void Start(TimeSpan timeout) {
var readySubject = new AsyncSubject<Unit>();
ThreadPool.QueueUserWorkItem(_ => AsyncStart(readySubject, TimeSpan.FromSeconds(1)));
// Point C - see below
readySubject.Timeout(timeout).First();
}
void AsyncStart(ISubject<Unit> readySubject, TimeSpan delay) {
Thread.Sleep(delay);
readySubject.OnNext(new Unit());
readySubject.OnCompleted();
}
void开始(TimeSpan超时){
var readySubject=new AsyncSubject();
ThreadPool.QueueUserWorkItem(=>AsyncStart(readySubject,TimeSpan.FromSeconds(1));
//C点——见下文
Timeout(Timeout.First();
}
void AsyncStart(ISubject readySubject,TimeSpan delay){
睡眠(延迟);
OnNext(新单元());
readySubject.OnCompleted();
}
有趣的是,C点的延迟超过了
AsyncStart
完成所需的时间AsyncSubject
保留上次发送的通知,Timeout
和First
仍将按预期执行。因此,关于Rx,我想很多人(包括我自己!)一开始都会做一件事:如果您使用任何传统的线程功能,如ResetEvents、Thread.Sleeps或其他,你做错了(tm)-这就像在LINQ中对数组进行强制转换一样,因为你知道底层类型恰好是数组
要知道的关键是,异步func由一个返回IObservable
的函数表示——这是一种神奇的调味品,可以让您在某个操作完成时发出信号。下面是如何“Rx ify”一个更传统的异步函数,就像您在Silverlight web服务中看到的那样:
IObservable<byte[]> readFromNetwork()
{
var ret = new AsyncSubject();
// Here's a traditional async function that you provide a callback to
asyncReaderFunc(theFile, buffer => {
ret.OnNext(buffer);
ret.OnCompleted();
});
return ret;
}
我想进一步补充Paul的评论,添加WaitHandles意味着你做错了,直接使用主题通常意味着你也做错了 <>试着考虑你的RX代码与序列或流水线一起工作。主题提供读写功能,这意味着你不再使用管道或序列(除非你有双向的管道或可以反转的序列?!) 首先,保罗的代码很酷,但让我们“滚开” 1st异步启动方法将其更改为
IObservable<Unit> AsyncStart(TimeSpan delay)
{
Observable.Timer(delay).Select(_=>Unit.Default);
}
现在我们有了一个清晰的管道或事件序列
异步启动-->isReady-->启动
而不是开始-->异步开始-->开始
如果我对您的问题空间了解得更多,我相信我们可以想出一种更好的方法来解决这个问题,而不需要start方法的阻塞特性。你使用Rx的次数越多,你就会发现你对何时需要阻止、使用waithandles等的旧假设会被扔出窗外。谢谢你的回答。这个问题是在Rx发布之前发布的,从那时起API表面发生了很大变化。关于我的特殊问题,恐怕我不能把障碍物扔出窗外。我使用的API需要多阶段初始化,但是使用Rx允许我并行化其中的一些。我发现Rx对于多阶段初始化也非常有用,但是我也发现我仍然不需要阻塞。通常,您可以在所有部件就绪时引发一个事件,指示系统就绪。然后,您可以有效地进行选择(多个),以调用在调用之前会阻止的内容。哦,ps,我提供的代码将在过去3年中在所有Rx API上运行(在某些变体中)。如果您必须在执行D之前完成a、B和C,并且并行运行a、B和C,则必须在完成a时进行阻止,在执行D之前,B和C。Rx不能改变这一点。但是,这样做可以帮助您摆脱复杂的状态处理。当A+B+C完成时,您可以触发D的开始。具体地说,我的观点是,执行continuations不需要.First()或任何其他实际阻止线程的操作符。这是一种常见的误解,很容易导致代码出现问题。
// Make it into a sync function
byte[] results = readFromNetwork().First();
// Keep reading blocks one at a time until we run out
readFromNetwork().Repeat().TakeUntil(x => x == null || x.Length == 0).Subscribe(bytes => {
Console.WriteLine("Read {0} bytes in chunk", bytes.Length);
})
// Read the entire stream and get notified when the whole deal is finished
readFromNetwork()
.Repeat().TakeUntil(x => x == null || x.Length == 0)
.Aggregate(new MemoryStream(), (ms, bytes) => ms.Write(bytes))
.Subscribe(ms => {
Console.WriteLine("Got {0} bytes in total", ms.ToArray().Length);
});
// Or just get the entire thing as a MemoryStream and wait for it
var memoryStream = readFromNetwork()
.Repeat().TakeUntil(x => x == null || x.Length == 0)
.Aggregate(new MemoryStream(), (ms, bytes) => ms.Write(bytes))
.First();
IObservable<Unit> AsyncStart(TimeSpan delay)
{
Observable.Timer(delay).Select(_=>Unit.Default);
}
void Start(TimeSpan timeout)
{
var isReady = AsyncStart(TimeSpan.FromSeconds(1))
.SubscribeOn(Scheduler.ThreadPool)
.PublishLast();
isReady.Connect();
isReady.Timeout(timeout).First();
}