我有从数据库加载的数据记录流。 我无法将所有内容存储并加载到内存中,因为它们有数百万个。 调用者应该逐个处理记录(当然我不能保证)。
我的第一次尝试是返回IEnumerable<Records>
的延迟序列,该序列将按需加载并由yield return
语句返回。
但我无法在此方法中使用await/async
(用于从数据库中获取数据),因为yield return
要求返回类型为IEnumerable<>
。
结果我无法使用async
和Task<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服务。
答案 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
分成两个区块。
SqlDataReader
以运行查询。你可以await
这部分。SqlDataReader
并使用yield return
迭代它。以下是一个示例,它基于您的代码示例。
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
}
}