提供请求数据,延迟加载(相当于收益率)

时间:2015-02-24 10:44:35

标签: c# .net asynchronous system.reactive

我有从数据库加载的数据记录流。 我无法将所有内容存储并加载到内存中,因为它们有数百万个。 调用者应该逐个处理记录(当然我不能保证)。

我的第一次尝试是返回IEnumerable<Records>的延迟序列,该序列将按需加载并由yield return语句返回。

但我无法在此方法中使用await/async(用于从数据库中获取数据),因为yield return要求返回类型为IEnumerable<>。 结果我无法使用asyncTask<IEnumerable<>>

阅读this说服我尝试使用Reactive Extensions,因为我可以等待异步方法并返回IObservable<>

但据我所知,只要有人订阅了我的observable,就会调用拉取数据的方法,它会立即提取所有数据。

这是我的方法的一部分,我的方法是这样的:

IList<int> ids = (...);
return Observable.Create<NitemonkeyRegistration>(async obs => 
    {
        using (SqlDataReader reader = await command.ExecuteReaderAsync())
            {
                if (!reader.HasRows)
                    obs.OnCompleted();

                while (await reader.ReadAsync())
                    ids.Add(reader.GetInt32(reader.GetOrdinal("RegistrationId")));

                for (int i = 0; i < ids.Count; i += 1000)
                {
                    //heavy database operations
                    var registrations = await GetRegistrationsByIds(connection, ids.Skip(i).Take(1000));
                    foreach (var pulledReg in registrations)
                    {
                        obs.OnNext(pulledReg);
                    }

                }

            }
     });

我可以将调用者置于控制之中吗?当他在observable上调用.Next()时,我的代码会根据需要提取数据吗?

如何使用反应式扩展程序实现与yield return类似的内容?

更新

这是我的消费者代码:

var cancellationTokenSource = new CancellationTokenSource();
await Observable.ForEachAsync<NitemonkeyRegistration>(niteMonkeySales, async (record, i) =>
            {
                try
                {
                     await SomethingAwaitableWhichCanTakeSeconds(record);
                }
                catch(Exception e)
                {
                    // add logging
                    // this cancels the loop but also the IObservable
                    cancellationTokenSource.Cancel();
                    // can't rethrow because line
                    // above will cause errored http response already created
                }
            }, cancellationTokenSource.Token);

问题在于推迟了新记录,而不是等待等待等待任务完成。我可以用.Wait()而不是异步lambda来做这个,但线程将被浪费等待冗长的网络操作完成。

可能很重要:这是一个ASP.NET WEB API服务。

3 个答案:

答案 0 :(得分:2)

Rx允许描述&#34;推送序列&#34;,其中生产者将值推送给观察者。如果你的要求是&#34;拉&#34;来自消息来源的值,我认为你要找的是Interactive Extensions Async Library(结账this Channel 9 Video)。它定义了IAsyncEnumerable<T>类型以及一整套LINQ运算符,它允许描述具有异步行为的基于拉的序列(但缺点是yield收益率不适用于该类型(yet至少) ),所以你可能需要编写自己的IAsyncEnumerator<T>实现。

答案 1 :(得分:0)

您是否需要使用反应性扩展?

你的第一次尝试可能是在正确的轨道上。

查看另一个问题的this answer

问题可能在于查询而不是客户端代码。

如链接问题所述,您可能必须重写查询以确保它正确地将数据流式传输到客户端。


<强>更新

你应该尝试将GetRegistrationsById分成两个区块。

  1. 获取SqlDataReader以运行查询。你可以await这部分。
  2. 使用返回的SqlDataReader并使用yield return迭代它。
  3. 以下是一个示例,它基于您的代码示例。

        IList<int> ids = new List<int>();
    
        private async void doWork()
        {
            var connection = new SqlConnection(...);
            connection.Open();
    
            SqlCommand command = new SqlCommand("SELECT registrationId FROM someTable", connection);
    
            using (SqlDataReader reader = await command.ExecuteReaderAsync())
            {
                while (await reader.ReadAsync())
                {
                    ids.Add(reader.GetInt32(reader.GetOrdinal("RegistrationId")));
                }
    
                reader.Close();
    
                //heavy database operations
                // Part 1 of whatever GetRegistrationsByIds does would go into GetRegistrationReader().
                var registrationReader = await Task.Run(() => GetRegistrationReader(connection, ids));
    
                // Part 2 of whatever GetRegistrationsByIds does for each 
                // Registration would go into GetRegistrations().
                var registrationEnumerator = GetRegistrations(orderReader);
    
                foreach (var registration in registrationEnumerator)
                {
                    // Do whatever you need to do for each registration
                    listBox1.Items.Add(registration.Id);
                }
    
            }
        }
    
        private IEnumerable<Registration> GetRegistrations(SqlDataReader reader)
        {
            while (reader.Read())
            {
                // You would do whatever you need to do to each registration here.
                var registration = new Registration{ Id = reader.GetInt32(reader.GetOrdinal("RegistrationId")) };
                yield return registration;
            }
    
        }
    
        private SqlDataReader GetRegistrationReader(SqlConnection connection, IList<int> ints)
        {
            // Some query that returns a lot of rows.
            // Ideally it would written to stream directly from the 
            // database server, rather than buffer the data to the client
            // side.
            SqlCommand command = new SqlCommand("SELECT * from registrations", connection);
    
            return command.ExecuteReader();
        }
    
        internal class Registration
        {
            public int Id;
            // ... other fields, etc.
        }
    

答案 2 :(得分:0)

Rx.NET目前没有很多内置的背压运营商。

使用像TPL Dataflow这样的东西可能更适合你的问题。

无论如何,我认为您可以使用BlockingCollection来限制从数据库中提取的费率:

// maximum of 10 items in buffer
var buffer = new BlockingCollection<NitemonkeyRegistration>(10);
niteMonkeySales.Subscribe(t => buffer.Add(t), () => buffer.CompleteAdd());

foreach (var item in buffer.GetConsumingEnumerable())
{
    try
    {
         await SomethingAwaitableWhichCanTakeSeconds(record);
    }
    catch(Exception e)
    {
        // add logging
        // this cancels the loop but also the IObservable
        cancellationTokenSource.Cancel();
        // can't rethrow because line
        // above will cause errored http response already created
    }
}