简体   繁体   English

退订后,我如何等待一切按Rx可观察的顺序进行?

[英]How can I await that everything is done in a Rx observable sequence after unsubscribe?

Introduction 介绍

In my WPF C# .NET application I use the reactive extensions (Rx) to subscribe to events and I often have to reload something from the DB to get the values I need to update the UI, because the event objects often only contains IDs and some meta data. 在我的WPF C#.NET应用程序中,我使用反应性扩展(Rx)订阅事件,而且我经常不得不从数据库中重新加载某些内容以获取更新UI所需的值,因为事件对象通常仅包含ID和一些元数据。

I use the Rx scheduling to load the data in the background and update the UI on the dispatcher. 我使用Rx调度在后台加载数据并更新调度程序上的UI。 I have made some bad experience with mixing "Task.Run" inside of a Rx sequence (when using "SelectMany" the order is no longer guaranteed and it is hard to control the scheduling in UnitTests). 我在Rx序列中混合“ Task.Run”时遇到了一些不好的经验(当使用“ SelectMany”时,将不再保证顺序,并且很难在UnitTests中控制调度)。 See also: Executing TPL code in a reactive pipeline and controlling execution via test scheduler 另请参阅: 在反应性管道中执行TPL代码并通过测试调度程序控制执行

My problem 我的问题

If I shutdown my app (or close a tab) I want to unsubscribe and then await the DB call (which is called from a Rx "Select") that still can be running after "subscription.Dispose". 如果我关闭我的应用程序(或关闭选项卡),我想取消订阅,然后等待DB调用(从Rx“ Select”调用),该调用在“ subscription.Dispose”之后仍然可以运行。 Until now I haven't found any good utility or easy way to do that. 到目前为止,我还没有找到任何好的工具或简便的方法来做到这一点。

Questions 问题

Is there any framework support to await everything still running in a Rx chain? 是否有任何框架支持来等待仍在Rx链中运行的所有内容

If not, do you have any good ideas how to make a easy to use utility? 如果没有,您对如何制作易于使用的实用程序有什么好主意?

Are there any good alternative ways to achieve the same? 有没有其他好的方法可以达到相同的目的?

Example

public async Task AwaitEverythingInARxChain()
{
    // In real life this is a hot observable event sequence which never completes
    IObservable<int> eventSource = Enumerable.Range(1, int.MaxValue).ToObservable();

    IDisposable subscription = eventSource
        // Load data in the background
        .ObserveOn(Scheduler.Default)
        .Select(id => LoadFromDatabase(id))

        // Update UI on the dispatcher
        .ObserveOn(DispatcherScheduler.Current)
        .SubscribeOn(Scheduler.Default) // In real life the source produces the event values on a background thread.
        .Subscribe(loadedData => UpdateUi(loadedData));

    Thread.Sleep(TimeSpan.FromSeconds(10));
// In real life I want to cancel (unsubscribe) here because the user has closed the Application or closed the tab and return a task which completes when everything is done.

    // Unsubscribe just guarantees that no "OnNext" is called anymore, but it doesn't wait until all operations in the sequence are finished (for example "LoadFromDatabase(id)" can still be runnig here.
    subscription.Dispose();

    await ?; // I need to await here, so that i can be sure that no "LoadFromDatabase(id)" is running anymore.

    ShutDownDatabase();
}

What I already tried (and didn't worked) 我已经尝试过(但没有用)的内容

  • Using the "Finally" operator to set the result of a TaskCompletionSource. 使用“最终”运算符设置TaskCompletionSource的结果。 The problem with this approach: Finally gets called directly after unsubscribing and "LoadFromDatabase" can still be running 这种方法的问题:退订后最终直接被调用,并且“ LoadFromDatabase”仍然可以运行

UPDATE: Example with console output and TakeUntil 更新:具有控制台输出和TakeUntil的示例

public async Task Main()
{
    Observable
        .Timer(TimeSpan.FromSeconds(5.0))
        .Subscribe(x =>
        {
            Console.WriteLine("Cancel started");
            _shuttingDown.OnNext(Unit.Default);
        });

    await AwaitEverythingInARxChain();
    Console.WriteLine("Cancel finished");
    ShutDownDatabase();
    Thread.Sleep(TimeSpan.FromSeconds(3));
}

private Subject<Unit> _shuttingDown = new Subject<Unit>();

public async Task AwaitEverythingInARxChain()
{
    IObservable<int> eventSource = Observable.Range(0, 10);

    await eventSource
        .ObserveOn(Scheduler.Default)
        .Select(id => LoadFromDatabase(id))
        .ObserveOn(Scheduler.Default)
        .TakeUntil(_shuttingDown)
        .Do(loadedData => UpdateUi(loadedData));
}

public int LoadFromDatabase(int x)
{
    Console.WriteLine("Start LoadFromDatabase: " + x);
    Thread.Sleep(1000);
    Console.WriteLine("Finished LoadFromDatabase: " + x);

    return x;
}

public void UpdateUi(int x)
{
    Console.WriteLine("UpdateUi: " + x);
}

public void ShutDownDatabase()
{
    Console.WriteLine("ShutDownDatabase");
}

Output (actual): 输出(实际):

Start LoadFromDatabase: 0
Finished LoadFromDatabase: 0
Start LoadFromDatabase: 1
UpdateUi: 0
Finished LoadFromDatabase: 1
Start LoadFromDatabase: 2
UpdateUi: 1
Finished LoadFromDatabase: 2
Start LoadFromDatabase: 3
UpdateUi: 2
Finished LoadFromDatabase: 3
Start LoadFromDatabase: 4
UpdateUi: 3
Cancel started
Cancel finished
ShutDownDatabase
Finished LoadFromDatabase: 4
Start LoadFromDatabase: 5
Finished LoadFromDatabase: 5
Start LoadFromDatabase: 6
Finished LoadFromDatabase: 6
Start LoadFromDatabase: 7

Expected: I want to have a guarantee that following are the last Outputs: 预期:我想保证以下是最后的输出:

Cancel finished
ShutDownDatabase

This is easier than you think. 这比您想象的要容易。 You can await observables. 您可以await观察。 So simply do this: 因此,只需执行以下操作:

public async Task AwaitEverythingInARxChain()
{
    IObservable<int> eventSource = Enumerable.Range(1, 10).ToObservable();

    await eventSource
        .ObserveOn(Scheduler.Default)
        .Select(id => LoadFromDatabase(id))
        .ObserveOn(DispatcherScheduler.Current)
        .Do(loadedData => UpdateUi(loadedData), () => ShutDownDatabase());
}

With a bit of Console.WriteLine action in your methods, and a little thread sleeping in the db call to simulate network delay, I get this output: 在您的方法中使用Console.WriteLine操作,并在db调用中休眠一个小线程以模拟网络延迟,我得到以下输出:

LoadFromDatabase: 1
LoadFromDatabase: 2
UpdateUi: 1
LoadFromDatabase: 3
UpdateUi: 2
LoadFromDatabase: 4
UpdateUi: 3
LoadFromDatabase: 5
UpdateUi: 4
LoadFromDatabase: 6
UpdateUi: 5
LoadFromDatabase: 7
UpdateUi: 6
LoadFromDatabase: 8
UpdateUi: 7
LoadFromDatabase: 9
UpdateUi: 8
LoadFromDatabase: 10
UpdateUi: 9
UpdateUi: 10
ShutDownDatabase

If you need to end the query, just create a shuttingDown subject: 如果需要结束查询,只需创建一个shuttingDown主题:

private Subject<Unit> _shuttingDown = new Subject<Unit>();

...and then modify the query like this: ...然后像这样修改查询:

    await eventSource
        .ObserveOn(Scheduler.Default)
        .Select(id => LoadFromDatabase(id))
        .ObserveOn(DispatcherScheduler.Current)
        .Do(
            loadedData => UpdateUi(loadedData),
            () => ShutDownDatabase())
        .TakeUntil(_shuttingDown);

You just need issue a _shuttingDown.OnNext(Unit.Default); 您只需要发出_shuttingDown.OnNext(Unit.Default); to unsubscribe the observable. 取消订阅可观察者。


Here's my complete working test code: 这是我完整的工作测试代码:

async Task Main()
{
    Observable
        .Timer(TimeSpan.FromSeconds(5.0))
        .Subscribe(x => _shuttingDown.OnNext(Unit.Default));

    await AwaitEverythingInARxChain();
}

private Subject<Unit> _shuttingDown = new Subject<Unit>();

public async Task AwaitEverythingInARxChain()
{
    IObservable<int> eventSource = Observable.Range(0, 10);

    await eventSource
        .ObserveOn(Scheduler.Default)
        .Select(id => LoadFromDatabase(id))
        .ObserveOn(DispatcherScheduler.Current)
        .Finally(() => ShutDownDatabase())
        .TakeUntil(_shuttingDown)
        .Do(loadedData => UpdateUi(loadedData));
}

public int LoadFromDatabase(int x)
{
    Console.WriteLine("LoadFromDatabase: " + x);
    Thread.Sleep(1000);
    return x;
}

public void UpdateUi(int x)
{
    Console.WriteLine("UpdateUi: " + x);
}

public void ShutDownDatabase()
{
    Console.WriteLine("ShutDownDatabase");
}

I get this output: 我得到以下输出:

LoadFromDatabase: 0
LoadFromDatabase: 1
UpdateUi: 0
LoadFromDatabase: 2
UpdateUi: 1
LoadFromDatabase: 3
UpdateUi: 2
LoadFromDatabase: 4
UpdateUi: 3
LoadFromDatabase: 5
UpdateUi: 4
ShutDownDatabase

Note that the observable tries to produce 10 values over 10 seconds, but it is cut short by the OnNext . 请注意,可观察对象尝试在10秒内生成10个值,但是它被OnNext缩短了。

I finally found a solution myself. 我终于找到了解决方案。 You can use TakeWhile to achive it. 您可以使用TakeWhile实现它。 TakeUntil does not work, because the main observable sequence immediately completes when the second observable sequence produces the first value. TakeUntil不起作用,因为当第二个可观察序列产生第一个值时,主要可观察序列立即完成。

Here is a example of the working solution: 这是工作解决方案的示例:

     public async Task Main_Solution()
    {
        CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

        Observable
            .Timer(TimeSpan.FromSeconds(4))
            .Subscribe(x =>
            {
                Console.WriteLine("Cancel startedthread='{0}'", Thread.CurrentThread.ManagedThreadId);
                cancellationTokenSource.Cancel();
            });

        await AwaitEverythingInARxChain(cancellationTokenSource.Token);
        Console.WriteLine("Cancel finished thread='{0}'", Thread.CurrentThread.ManagedThreadId);
        ShutDownDatabase();
        Thread.Sleep(TimeSpan.FromSeconds(10));
    }

    public async Task AwaitEverythingInARxChain(CancellationToken token)
    {
        IObservable<int> eventSource = Observable.Range(0, 10);

        await eventSource
            .ObserveOn(Scheduler.Default)
            .Select(id => LoadFromDatabase(id))
            .TakeWhile(_ => !token.IsCancellationRequested)
            .ObserveOn(Scheduler.Default) // Dispatcher in real life
            .Do(loadedData => UpdateUi(loadedData)).LastOrDefaultAsync();
    }

    public int LoadFromDatabase(int x)
    {
        Console.WriteLine("Start LoadFromDatabase: {0} thread='{1}'", x, Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(TimeSpan.FromSeconds(3));
        Console.WriteLine("Finished LoadFromDatabase: {0} thread='{1}'", x, Thread.CurrentThread.ManagedThreadId);

        return x;
    }

    public void UpdateUi(int x)
    {
        Console.WriteLine("UpdateUi: '{0}' thread='{1}'", x, Thread.CurrentThread.ManagedThreadId);
    }

    public void ShutDownDatabase()
    {
        Console.WriteLine("ShutDownDatabase thread='{0}'", Thread.CurrentThread.ManagedThreadId);
    }

And the output: 并输出:

Start LoadFromDatabase: 0 thread='9'
Finished LoadFromDatabase: 0 thread='9'
Start LoadFromDatabase: 1 thread='9'
UpdateUi: '0' thread='10'
Cancel startedthread='4'
Finished LoadFromDatabase: 1 thread='9'
Cancel finished thread='10'
ShutDownDatabase thread='10'

Note that "ShutDownDatabase" is the last output (as expected). 请注意,“ ShutDownDatabase”是最后的输出(按预期)。 It waits until "LoadFromDatabase" is finished for the second value, even if its produced value is not further processed. 它等待直到“ LoadFromDatabase”完成第二个值,即使其产生的值未得到进一步处理也是如此。 This is exactly what I want. 这正是我想要的。

You need to have something to await on. 您需要等待一些事情。 You can't await on a subscription disposal. 您不能等待订阅处置。 The easiest way to do it would be to turn your disposal logic into part of the observable itself: 最简单的方法是将您的处置逻辑变成可观察的一部分:

var observable = eventSource
    // Load data in the background
    .ObserveOn(Scheduler.Default)
    .Select(id => LoadFromDatabase(id))
    .TakeUntil(Observable.Timer(TimeSpan.FromSeconds(10))) //This replaces your Thread.Sleep call
    .Publish()
    .RefCount();

var subscription = observable.ObserveOn(DispatcherScheduler.Current)
    .Subscribe(loadedData => UpdateUi(loadedData));

//do whatever you want here.

await observable.LastOrDefault();

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM