问题
I have two IDisposables
that I need to have disposed in sequential order. The ordering is important since the first IDisposable
kills an Rx subscription that is relying on a service that will be killed by the second IDisposable
. This is within a Windows Forms application where the subscription of the IObservable
needs to happen on a different thread but the observing and disposing needs to happen on the UI thread. (Actually, I don't care if the disposing happens on the UI thread as long as the order is ensured.) So, in code I have roughly the following (once reduced):
SomeService = new DisposableService();
Subscription = Foo(someService).SubscribeOn(NewThreadScheduler.Default).ObserveOn(theForm).Subscribe(...)
On a number of UI events I need to dispose of both of these in order (Subscription and then SomeService). To do this I have tried using Rx's CompositeDisposable
in addition to the ContextDisposable
to provide serial disposing on the same thread:
_Disposable = new CompositeDisposable(new[] {
new ContextDisposable(WindowsFormsSynchronizationContext.Current, Subscription),
new ContextDisposable(WindowsFormsSynchronizationContext.Current, SomeService)});
The above does not work however. Based on my logging _Disposable
and the ContextDisposable
for SomeService
are called on the same thread but the ContextDisposable
is still occurring on a different thread concurent with the service being disposed (and thus resulting in race conditions and NPEs).
I've only been programming C# for a couple of weeks and so I'm sure the problem is my misunderstanding of how contexts and dispatchers work. What is the correct approach to this problem?
回答1:
Unless I'm misunderstanding something, you can control which thread disposes what. Who subscribes on which thread doesn't matter. Look at this example
internal class Program
{
private static void Main(string[] args)
{
ReactiveTest rx1 = null;
ReactiveTest rx2 = null;
var thread1 = new Thread(() => rx1 = new ReactiveTest());
var thread2 = new Thread(() => rx2 = new ReactiveTest());
thread1.Start();
thread2.Start();
Thread.Sleep(TimeSpan.FromSeconds(1));
thread1.Join();
thread2.Join();
rx1.Dispose();
rx2.Dispose();
}
}
public class ReactiveTest : IDisposable
{
private IDisposable _timerObservable;
private object _lock = new object();
public ReactiveTest()
{
_timerObservable = Observable.Interval(TimeSpan.FromMilliseconds(250)).Subscribe(i =>
Console.WriteLine("[{0}] - {1}", Thread.CurrentThread.ManagedThreadId, i));
}
public void Dispose()
{
lock (_lock)
{
_timerObservable.Dispose();
Console.WriteLine("[{0}] - DISPOSING", Thread.CurrentThread.ManagedThreadId);
}
}
}
This outputs
[14] - 0
[7] - 0
[15] - 1
[7] - 1
[14] - 2
[15] - 2
[10] - DISPOSING
[10] - DISPOSING
You can see we subscribed on two seperate threads, then disposed on a third. I only locked the dispose in case you had something threadsafe that needs to happen in the subscription. In this example it's unnecessary really.
回答2:
SubscribeOn schedules the calls to both Subscribe
and Dispose
. Therefore, calling Dispose
on your Subscription variable, regardless of whether execution is currently on the UI thread or not, results in the subscription being scheduled for disposal by NewThreadScheduler.Default
.
It's almost never a good idea to use SubscribeOn
; however, in your case you're claiming that it solves 50% of your problem - and that's 50% more than most uses I've seen - so I must question whether or not you actually need the subscription to execute on a background thread in the first place. It's awfully expensive to create a brand new thread and then invoke a method on it, compared to just calling a method on the UI thread directly, if all the method does is begin some asynchronous work such as sending a network request or reading a file. Perhaps if computing a network message to be sent is proven to be overly time-consuming, then using SubscribeOn
may be correct; although, of course, only if you want disposal scheduled as well.
If the subscription to your observable must execute on a background thread, yet disposal must remain free-threaded, then consider using the following operator instead (untested).
public static class ObservableExtensions
{
public static IObservable<TSource> SubscribeOn<TSource>(
this IObservable<TSource> source,
bool doNotScheduleDisposal,
IScheduler scheduler)
{
if (!doNotScheduleDisposal)
{
return source.SubscribeOn(scheduler);
}
return Observable.Create<TSource>(observer =>
{
// Implementation is based on that of the native SubscribeOn operator in Rx
var s = new SingleAssignmentDisposable();
var d = new SerialDisposable();
d.Disposable = s;
s.Disposable = scheduler.Schedule(() =>
{
d.Disposable = source.SubscribeSafe(observer);
});
return d;
});
}
}
来源:https://stackoverflow.com/questions/14126971/ensuring-sequential-disposing-of-multiple-idisposables