-
Notifications
You must be signed in to change notification settings - Fork 751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Async / Await with IObserver<T> #459
Comments
If I understand you correctly, you would like something like this (however, as you pointed out properly, this should not be done like this): // !!! should NOT do this, because Subscribe cannot handle an async function properly !!!
Observable.Interval(TimeSpan.FromSeconds(1))
.Subscribe(async number => await DoSomeWorkAsync(number)); // !!! Subscribe does not (a)wait for this async lambda So, this should not be done, because Of course, you should not wait for the execution of the So, in case of an async operation (which is probably long running, that is why it is async in the first place) you should not call it from inside the Observable.Interval(TimeSpan.FromSeconds(1))
.Select(number => Observable.FromAsync(async () => await DoSomeWorkAsync(number)))
.Concat()
.Subscribe(); or: Observable.Interval(TimeSpan.FromSeconds(1))
.Select(number => Observable.FromAsync(async () => await ReturnSomethingAsync(number)))
.Concat()
.Subscribe(number => Console.WriteLine($"number: {number}")); In the above examples,
async Task DoSomeWorkAsync(long number)
{
Console.WriteLine($"DoSomeWorkAsync BEGIN '{number}'");
await Task.Delay(TimeSpan.FromSeconds(3));
Console.WriteLine($"DoSomeWorkAsync END '{number}'");
}
async Task<long> ReturnSomethingAsync(long number)
{
Console.WriteLine($"ReturnSomethingAsync BEGIN '{number}'");
await Task.Delay(TimeSpan.FromSeconds(3));
Console.WriteLine($"ReturnSomethingAsync END '{number}'");
return number * 10;
} If you use the And if you were to execute a long running sync method, you should use Observable.Interval(TimeSpan.FromSeconds(1))
.Select(number => Observable.Defer(() => Observable.Start(() => LongRunningWork(number))))
.Concat()
.Subscribe(); BTW, |
Thank you. This is helpful. My next concern though is more from a pattern / design perspective. It appears the only way to handle an observer that may take some time (use Tasks) is to implement it as an observable. From a design perspective it seems wrong to implement what is logically an observer as an observable. Where am I wrong? Have I missed something with the observer pattern, Rx.NET design, other? |
Actually, these Rx LINQ methods (which take In a way, these methods are similar to the An observer is something that can subscribe to and react to an observable; it defines what should be done when a next item is yielded, or upon completion/error. So, if we took an Rx LINQ method, excluded its So, this source
.Select(number => Observable.FromAsync(async () => await DoSomeWorkAsync(number))) Now, to be honest, when I started learning Rx, I also found it strange that there were no such thing as public static class MyObservableExtensions
{
public static IDisposable SubscribeAsync<T>(this IObservable<T> source, Func<Task> onNextAsync) =>
source
.Select(number => Observable.FromAsync(onNextAsync))
.Concat()
.Subscribe();
public static IDisposable SubscribeAsyncConcurrent<T>(this IObservable<T> source, Func<Task> onNextAsync) =>
source
.Select(number => Observable.FromAsync(onNextAsync))
.Merge()
.Subscribe();
public static IDisposable SubscribeAsyncConcurrent<T>(this IObservable<T> source, Func<Task> onNextAsync, int maxConcurrent) =>
source
.Select(number => Observable.FromAsync(onNextAsync))
.Merge(maxConcurrent)
.Subscribe();
} Here, On the other hand, Of course, you could use these Observable.Range(0, 30)
.Select(number => Observable.FromAsync(() => CalculateLongRunningTask1(number)))
.Merge(5)
.Select(number => Observable.FromAsync(() => CalculateLongRunningTask2(number)))
.Merge(10) Observable.Range(0, 30)
.Select(number => Observable.FromAsync(async () => await CalculateLongRunningTask1(number)))
.Select(number => Observable.FromAsync(async () => await CalculateLongRunningTask2(await number)))
.Merge(10) BTW, if we implemented these |
Perfect, thanks. This answers my questions. |
@ghuntley Please close. |
I just want to clarify - there is no build in way in Rx to handle async method in Subscribe? |
What would be really nice to have is equivalent of this (from RxJS): EDIT: Never mind, actually there is equivalent which works just fine: |
@davidnemeti thank you so much for these detailed clarifications. For exampleinstead of public static class MyObservableExtensions
{
public static IDisposable SubscribeAsync<T>(this IObservable<T> source, Func<Task> onNextAsync) =>
source
.Select(number => Observable.FromAsync(onNextAsync))
.Concat()
.Subscribe();
public static IDisposable SubscribeAsyncConcurrent<T>(this IObservable<T> source, Func<Task> onNextAsync) =>
source
.Select(number => Observable.FromAsync(onNextAsync))
.Merge()
.Subscribe();
public static IDisposable SubscribeAsyncConcurrent<T>(this IObservable<T> source, Func<Task> onNextAsync, int maxConcurrent) =>
source
.Select(number => Observable.FromAsync(onNextAsync))
.Merge(maxConcurrent)
.Subscribe();
} could we write public static class MyObservableExtensions
{
public static IDisposable SubscribeAsync<T>(this IObservable<T> source, Func<Task> onNextAsync) =>
source
.Select(number => onNextAsync().ToObservable()) // note ToObservable instead of FromAsync!
.Concat()
.Subscribe();
public static IDisposable SubscribeAsyncConcurrent<T>(this IObservable<T> source, Func<Task> onNextAsync) =>
source
.Select(number => onNextAsync().ToObservable()) // note ToObservable instead of FromAsync!
.Merge()
.Subscribe();
public static IDisposable SubscribeAsyncConcurrent<T>(this IObservable<T> source, Func<Task> onNextAsync, int maxConcurrent) =>
source
.Select(number => onNextAsync().ToObservable()) // note ToObservable instead of FromAsync!
.Merge(maxConcurrent)
.Subscribe();
} and if no, what is the difference between 'Observable.FromAsync' and '.ToObservable'? |
@gentledepp, The difference is that This difference is due to the fact that the parameter of It means that when you create an observable by using You can check this behavior by running the following example: void Main()
{
Observable.Interval(TimeSpan.FromSeconds(1))
.SubscribeAsync(number => DoSomeWorkAsync(number));
}
async Task DoSomeWorkAsync(long number)
{
Console.WriteLine($"DoSomeWorkAsync BEGIN '{number}'");
await Task.Delay(TimeSpan.FromSeconds(3));
Console.WriteLine($"DoSomeWorkAsync END '{number}'");
}
#if true
public static class MyObservableExtensions
{
public static IDisposable SubscribeAsync<T>(this IObservable<T> source, Func<T, Task> onNextAsync) =>
source
.Select(number => Observable.FromAsync(() => onNextAsync(number)))
.Concat()
.Subscribe();
public static IDisposable SubscribeAsyncConcurrent<T>(this IObservable<T> source, Func<T, Task> onNextAsync) =>
source
.Select(number => Observable.FromAsync(() => onNextAsync(number)))
.Merge()
.Subscribe();
public static IDisposable SubscribeAsyncConcurrent<T>(this IObservable<T> source, Func<T, Task> onNextAsync, int maxConcurrent) =>
source
.Select(number => Observable.FromAsync(() => onNextAsync(number)))
.Merge(maxConcurrent)
.Subscribe();
}
#else
public static class MyObservableExtensions
{
public static IDisposable SubscribeAsync<T>(this IObservable<T> source, Func<T, Task> onNextAsync) =>
source
.Select(number => onNextAsync(number).ToObservable()) // note ToObservable instead of FromAsync!
.Concat()
.Subscribe();
public static IDisposable SubscribeAsyncConcurrent<T>(this IObservable<T> source, Func<T, Task> onNextAsync) =>
source
.Select(number => onNextAsync(number).ToObservable()) // note ToObservable instead of FromAsync!
.Merge()
.Subscribe();
public static IDisposable SubscribeAsyncConcurrent<T>(this IObservable<T> source, Func<T, Task> onNextAsync, int maxConcurrent) =>
source
.Select(number => onNextAsync(number).ToObservable()) // note ToObservable instead of FromAsync!
.Merge(maxConcurrent)
.Subscribe();
}
#endif If you use the |
@davidnemeti thank you so much for this explanation! Suddenly everything makes sense. So I would use Can I buy you a beer somehow? 🍺 |
@gentledepp, you are welcome. Unfortunately, I don't like beer, so a simple "like" would be enough. :-) Regarding your question about The point is that you should try to avoid working with pure Sure, if you only have a Thus, since you can work with Personally, I do not use On the other hand, |
@davidnemeti sorry for necroposting, but I am wondering if it is possible, in your first example, to asynchronously handle not every element of the first sequence:
|
@wh1t3cAt1k sure you can achieve that. You could use the Observable.Interval(TimeSpan.FromSeconds(1))
.Latest()
.Select(number => Observable.FromAsync(async () => await DoSomeWorkAsync(number)))
.Concat(); E.g. it blocks until the first element arrives. A more sophisticated solution could be to introduce our own function which is non-blocking and you can even introduce concurrency if needed: public static class MyObservableExtensions
{
public static IObservable<TResult> SelectAndOmit<T, TResult>(this IObservable<T> source, Func<T, IObservable<TResult>> process, Action<T> noProcess, int maximumConcurrencyCount = 1)
{
var semaphore = new SemaphoreSlim(initialCount: maximumConcurrencyCount, maxCount: maximumConcurrencyCount);
return source
.SelectMany(item =>
{
if (semaphore.Wait(millisecondsTimeout: 0))
{
return Observable.Return(process(item).Finally(() => { semaphore.Release(); }));
}
else
{
noProcess(item);
return Observable.Empty<IObservable<TResult>>();
}
})
.Merge(maximumConcurrencyCount);
}
} You can use like this: Observable.Interval(TimeSpan.FromSeconds(1))
.SelectAndOmit(
number => Observable.FromAsync(async () => await DoSomeWorkAsync(number)),
number => Console.WriteLine($"no work {number}")
); With a maximum number of 2 concurrent processes: Observable.Interval(TimeSpan.FromSeconds(1))
.SelectAndOmit(
number => Observable.FromAsync(async () => await DoSomeWorkAsync(number)),
number => Console.WriteLine($"no work {number}"),
maximumConcurrencyCount: 2
); |
@davidnemeti thanks a lot! It looks like there are multiple ways to achieve this, as usual with Rx. Someone helped me out on StackOverflow, too: see "ExhaustMap" implementation: https://stackoverflow.com/questions/64353907/how-can-i-implement-an-exhaustmap-handler-in-rx-net However, it does not have concurrency control as your solution does. |
- [Changes API: How to Subscribe to Document Changes | RavenDB 5.3 Documentation](https://ravendb.net/docs/article-page/5.3/csharp/client-api/changes/how-to-subscribe-to-document-changes#fordocumentsincollection) - [Async / Await with IObserver<T> · Issue #459 · dotnet/reactive](dotnet/reactive#459) - [Change Streams](https://mongodb.github.io/mongo-csharp-driver/2.9/reference/driver/change_streams/)
I am wondering if the approach suggested by @davidnemeti in this thread is still the best approach you could use when you need to run an async method in a sync context, now that AsyncRX.NET NuGet is released, for .NET Standard 2.0 even (cc @idg10, I am the dude from Reddit comments who asked about .NET FX support). I was refactoring my code (currently using ReactiveUI with WinForms) and seeing as ReactiveUI and ReactiveMarbles.ObservableEvents are yet to catch up with AsyncRX.NET it seems that I either
From the sound of it, the second option sounds a bit icky to do. Maybe there are other approaches to solving the problem that I am not seeing after a look on the source code? |
AsyncRX.NET is currently still experimental, and that's partly because it's not yet clear what the full implications of using it in will be. For example, in the rxnet slack channel, @anaisbetts made this point:
which gets to the heart of something that will need to be resolved if AsyncRX.NET is to be used successfully in scenarios such as this. I haven't fully understood exactly what it is you're doing that has let to you to:
Did Ani already unblock you on the rxnet channel? If not, could you explain what you're doing that needs this? |
How to turn Subscribe(onNext, onError, onComplete) into async/await version, with 3 different callback? |
I would say that, create extension methods. Something like: public static IDisposable SubscribeAsync<T> (this IObservable<T> source, Func<T, Task> onNextAsync)
{
return source.SubscribeAsync(onNextAsync, ex => throw ex, () => { });
}
public static IDisposable SubscribeAsync<T> (this IObservable<T> source, Func<T, Task> onNextAsync, Action onCompleted)
{
return source.SubscribeAsync(onNextAsync, ex => throw ex, onCompleted);
}
public static IDisposable SubscribeAsync<T> (this IObservable<T> source, Func<T, Task> onNextAsync, Action<Exception> onError)
{
return source.SubscribeAsync(onNextAsync, onError, () => { });
}
public static IDisposable SubscribeAsync<T> (this IObservable<T> source,
Func<T, Task> onNextAsync, Action<Exception> onError, Action onCompleted)
{
return source
.Select(param => Observable.FromAsync(() => onNextAsync(param)))
.Merge()
.Subscribe(
unit => { },
ex => onError(ex),
onCompleted);
} Correct me if I am wrong. Edit: notice I am not re-throwing the exceptions and I am not sure about the consequences. |
So, I have a very simple eventing system which is surprising difficult it seems to make work right with Rx.NET. I have producers and consumers with an at least once message guarantee. The consumers utilize both IObserverable to pull messages from an event source log and IObserver to handle those messages. The handling / observing of the messages requires interfacing with external system which are wrapped in API's that return Tasks. Following best practices we should async / await the tasks all the way up the stack. Here comes the issue, IObserver does not support returning a Task. This means some sort of blocking operation must be done against the returned Task in the implementation of the IObserver which violates best practice and introduces a lot of risk for deadlocks and other threading issues depending on hosting platform. So, should I use Rx.NET or roll my own here. Is there a solution to this that I’m just not seeing?
The text was updated successfully, but these errors were encountered: