【问题标题】:Buffer data from database cursor while keeping UI responsive缓冲来自数据库游标的数据,同时保持 UI 响应
【发布时间】:2013-11-17 08:59:14
【问题描述】:

我有一个已填充的数据库目录和一个可用于检索对象的游标。这个目录显然可能非常大,我想做的是使用 ReactiveUI 来缓冲数据,同时保持 UI 数据绑定和响应式。我按照here 的步骤将我的IEnumerable 翻译成IObservable,如下所示:

public class CatalogService
{
   ...

   public IObservable<DbObject> DataSource
   {
        get
        {
            return Observable.Create<DbObject>(obs =>
            {
                var cursor = Database.Instance.GetAllObjects();
                var status = cursor.MoveToFirst();

                while (status == DbStatus.OK)
                {
                    var dbObject= Db.Create(cursor);
                    obs.OnNext(dbObject);

                    status = cursor.MoveToNext();
                }

                obs.OnCompleted();

                return Disposable.Empty;
            });
        }
    }
}

在我的视图类(特别是Loaded 事件)中,我订阅了数据源并使用缓冲区方法,希望保持 UI 响应。

    public ObservableCollection<DbObject> DbObjects { get; set; }

    private async void OnLoad(object sender, RoutedEventArgs e)
    {
        var observableData = CatalogService.Instance.DataSource.Publish();
        var chunked = observableData.Buffer(TimeSpan.FromMilliseconds(100));
        var dispatcherObs = chunked.ObserveOnDispatcher(DispatcherPriority.Background);
        dispatcherObs.Subscribe(dbObjects =>
        {
            foreach (var dbObject in dbObjects)
            {
                DbObjects.Add(dbObject);
            }
        });

        await Task.Run(() => observableData.Connect());
        await dispatcherObs.ToTask();
    }

不幸的是,结果恰恰相反。当我的视图控件(包含一个简单的ListBox 数据绑定到DbObjects 属性)加载时,它不会显示任何数据,直到整个目录被枚举。只有这样 UI 才会刷新。

我是 ReactiveUI 的新手,但我确信它能够胜任手头的任务。如果我使用不正确,有人有什么建议或指点吗?

【问题讨论】:

  • 问题:您如何确定 UI 不仅仅是简单地呈现结果,速度如此之快以至于您看不到它们? UI真的没有响应吗?数据库查询中第一个结果的时间和最后一个结果的时间是多少?结果集中有多少行?
  • 快速提问...这里的 IEnumerable 是什么对象?
  • @Christopher:没有什么是 IEnumerable。 DataSource 属性已从 IEnumerable 转换为 IObservable。
  • 抱歉,请说 IEnumerable。
  • @James:UI 实际上并非没有响应,但 ListBox 一直是空的。大约有一千行(图像),ListBox 在很长一段时间后从 0 个图像变为所有图像。

标签: c# wpf system.reactive reactiveui


【解决方案1】:

等待进一步的信息,我猜你可能有几个零长度缓冲区,具体取决于 DB 查询需要多长时间,然后是一个包含所有结果的非零长度缓冲区。您最好通过长度和时间来限制缓冲区大小。

编辑 - 我只是想对原始实现中涉及的各种线程进行分析。我不同意 Paul 的分析,我不相信 UI Thread 因为 DB 查询而被阻塞。我相信它是由于大量结果被缓冲而被阻止的。

Charlie - 请您在代码中(而不是使用调试器)对 DB 查询进行计时并转储您获得的缓冲区长度。

我将注释代码以显示所涉及的所有三个线程的顺序:

首先,在提供的代码之外,我假设通过Loaded 事件调用OnLoad

(1) - UI 线程调用 OnLoad

public ObservableCollection<DbObject> DbObjects { get; set; }

private async void OnLoad(object sender, RoutedEventArgs e)
{
    // (2) UI Thread enters OnLoad

    var observableData = CatalogService.Instance.DataSource.Publish();

    var chunked = observableData
        // (6) Thread A OnNext passes into Buffer
        .Buffer(TimeSpan.FromMilliseconds(100));
        // (7) Thread B, threadpool thread used by Buffer to run timer 

    var dispatcherObs = chunked
        // (8) Thread B still
        .ObserveOnDispatcher(DispatcherPriority.Background);
        // (9) Non blocking OnNexts back to UI Thread

    dispatcherObs.Subscribe(dbObjects =>
    {
        // (10) UI Thread receives buffered dbObjects            
        foreach (var dbObject in dbObjects)
        {
            // (11) UI Thread hurting while all these images are
            // stuffed in the collection in one go - This is the issue I bet.
            DbObjects.Add(dbObject);
        }
    });

    await Task.Run(() =>
    {
        // (3) Thread A - a threadpool thread,
        // triggers subscription to DataSource
        // UI Thread is *NOT BLOCKED* due to await
        observableData.Connect()
    });
    // (13) UI Thread - Dispatcher call back here at end of Create call
    // BUT UI THREAD WAS NOT BLOCKED!!!

    // (14) UI Thread - This task will be already completed
    // It is causing a second subscription to the already completed published observable
    await dispatcherObs.ToTask();


}

public class CatalogService
{
   ...

   public IObservable<DbObject> DataSource
   {
        get
        {
            return Observable.Create<DbObject>(obs =>
            {
                // (4) Thread A runs Database query synchronously
                var cursor = Database.Instance.GetAllObjects();
                var status = cursor.MoveToFirst();

                while (status == DbStatus.OK)
                {
                    var dbObject= Db.Create(cursor);
                    // (5) Thread A call OnNext
                    obs.OnNext(dbObject);

                    status = cursor.MoveToNext();
                }

                obs.OnCompleted();
                // (12) Thread A finally completes subscription due to Connect()
                return Disposable.Empty;
            });
        }
    }
}

我认为问题在于一个大型缓冲区一次性将大量结果卸载到 ObservableCollection 中,从而为列表框创建大量工作。

【讨论】:

  • 尝试限制项目的数量,一直到只有 10 个项目。结果一样——ListBox很长时间没有数据,最终所有的item都出现了。
  • 我认为詹姆斯的理论仍然成立。与迭代查询结果相比,数据库查询将花费大量时间。您遇到的延迟可能只是查询的执行时间。当然,除非结果是流式传输的,并且有大量的行......
  • 实际上,我在调试器中单步执行了查询,并验证执行时间不到一秒。请记住,这是一个数据库游标,而不是完整的结果。我一次“移动”光标一个元素,所以这不是一个非常占用 CPU 的操作。我的理论实际上是UI线程由于某种原因被阻塞,但我看不出是怎么回事。
  • 或者也许与其说是 UI 线程,不如说是数据绑定操作。
  • 所以这是一个想法,但我可能完全没有根据,因为我通常不会将 Rx.NET 用于 GUI 相关的东西......你正在观察调度程序,但你仍在订阅当前线程,我认为这可能会导致数据库迭代同步运行。这会导致问题吗?
【解决方案2】:

你的问题在这里:

           while (status == DbStatus.OK)
            {
                var dbObject= Db.Create(cursor);
                obs.OnNext(dbObject);

                status = cursor.MoveToNext();
            }

一旦有人订阅,该循环就会以阻塞方式同步运行。由于您在 UI 线程上创建订阅(在您调用 Connect 时),它将在 UI 线程上运行整个事情。将其更改为:

return Observable.Create<DbObject>(obs =>
{
    Observable.Start(() => {
        var cursor = Database.Instance.GetAllObjects();
        var status = cursor.MoveToFirst();

        while (status == DbStatus.OK)
        {
            var dbObject= Db.Create(cursor);
            obs.OnNext(dbObject);

            status = cursor.MoveToNext();
        }

        obs.OnCompleted();
    }, RxApp.TaskPoolScheduler);

    return Disposable.Empty;
});

【讨论】:

  • 嗨,保罗,感谢您的回答!我最近真的读了很多你的东西。不幸的是,这个改变没有帮助——结果是在启动时,我在前 10 秒内没有收到任何结果(奇怪,考虑到 100 毫秒的缓冲区?)。之后,我一次得到所有结果,无论我在 Buffer 中指定的桶大小。
  • 经过深思熟虑后,我认为问题在于一次查询所有对象;我认为我应该做的只是获取当前视口中的对象(例如,窗口的可见区域),并且随着用户滚动,增加起始索引但仍然只检索一小部分数据(例如, 20 项)。我认为我仍然可以使用 ReactiveUI 以声明方式完成此操作,但在设置它时仍然有些麻烦。 :(
  • Pauls 的想法是我的第一个想法 - 请参阅我的分析,了解为什么没有像这样阻塞 UI 线程。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-31
相关资源
最近更新 更多