C# 我怎样才能让‘等待…;’使用“yield return”(即在迭代器方法中)?
我的现有代码类似于:C# 我怎样才能让‘等待…;’使用“yield return”(即在迭代器方法中)?,c#,ado.net,async-await,generator,yield-return,C#,Ado.net,Async Await,Generator,Yield Return,我的现有代码类似于: IEnumerable<SomeClass> GetStuff() { using (SqlConnection conn = new SqlConnection(connectionString)) using (SqlCommand cmd = new SqlCommand(sql, conn) { conn.Open(); SqlDataReader reader = cmd.ExecuteReade
IEnumerable<SomeClass> GetStuff()
{
using (SqlConnection conn = new SqlConnection(connectionString))
using (SqlCommand cmd = new SqlCommand(sql, conn)
{
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
SomeClass someClass = f(reader); // create instance based on returned row
yield return someClass;
}
}
}
编译器通知我,await
只能在标记为async
的方法中使用,并建议我将方法签名修改为:
async Task<IEnumerable<SomeClass>> GetStuff()
异步任务GetStuff()
但是,这样做会使GetStuff()
无法使用,因为:
GetStuff()
的主体不能是迭代器块,因为Task
不是迭代器接口类型
我确信我缺少异步编程模型的一个关键概念
问题:
- 我可以在迭代器中使用
?怎么做ReadAsync()
- 我如何以不同的方式思考异步范例,以便了解它在这种情况下是如何工作的
- 问题是你所问的实际上没有多大意义
IEnumerable
是一个同步接口,返回Task
对您没有多大帮助,因为不管发生什么情况,某些线程都必须阻止等待每个项目
实际上,您想要返回的是IEnumerable
的一些异步替代方案:类似于IObservable
、来自TPL数据流的数据流块或计划添加到C#8.0/.Net Core 3.0中的IAsyncenuerable
。(同时,也有一些人控制着它。)
使用TPL数据流,一种方法是:
ISourceBlock<SomeClass> GetStuff() {
var block = new BufferBlock<SomeClass>();
Task.Run(async () =>
{
using (SqlConnection conn = new SqlConnection(connectionString))
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
await conn.OpenAsync();
SqlDataReader reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
SomeClass someClass;
// Create an instance of SomeClass based on row returned.
block.Post(someClass);
}
block.Complete();
}
});
return block;
}
ISourceBlock GetStuff(){
var block=新的缓冲块();
Task.Run(异步()=>
{
使用(SqlConnection conn=newsqlconnection(connectionString))
使用(SqlCommand cmd=newsqlcommand(sql,conn))
{
等待连接OpenAsync();
SqlDataReader=await cmd.ExecuteReaderAsync();
while(等待reader.ReadAsync())
{
某类某类;
//根据返回的行创建SomeClass的实例。
block.Post(someClass);
}
block.Complete();
}
});
返回块;
}
您可能希望将错误处理添加到上述代码中,但否则,它应该可以工作,并且将是完全异步的
然后,代码的其余部分也将异步使用返回块中的项,可能使用
ActionBlock
不,您当前不能将async与迭代器块一起使用。正如斯维克所说,要做到这一点,您需要像IAsyncEnumerable
这样的东西
如果您有返回值Task
,则表示函数返回单个Task
对象,该对象一旦完成,将为您提供一个完整格式的IEnumerable(此枚举中没有任务异步的空间)。任务对象完成后,调用方应该能够同步迭代它在可枚举表中返回的所有项
下面是一个返回任务
的解决方案。通过这样做,您可以从async中获得很大的好处:
async Task<IEnumerable<SomeClass>> GetStuff()
{
using (SqlConnection conn = new SqlConnection(""))
{
using (SqlCommand cmd = new SqlCommand("", conn))
{
await conn.OpenAsync();
SqlDataReader reader = await cmd.ExecuteReaderAsync();
return ReadItems(reader).ToArray();
}
}
}
IEnumerable<SomeClass> ReadItems(SqlDataReader reader)
{
while (reader.Read())
{
// Create an instance of SomeClass based on row returned.
SomeClass someClass = null;
yield return someClass;
}
}
…以及一个示例用法:
async void Caller()
{
// Calls get-stuff, which returns immediately with a Task
Task<IEnumerable<SomeClass>> itemsAsync = GetStuff();
// Wait for the task to complete so we can get the items
IEnumerable<SomeClass> items = await itemsAsync;
// Iterate synchronously through the items which are all already present
foreach (SomeClass item in items)
{
Console.WriteLine(item);
}
}
async void Caller()
{
// Synchronously get a list of Tasks
IEnumerable<Task<SomeClass>> items = GetStuff();
// Iterate through the Tasks
foreach (Task<SomeClass> itemAsync in items)
{
// Wait for the task to complete. We need to wait for
// it to complete before we can know if it's the end of
// the sequence
SomeClass item = await itemAsync;
// End of sequence?
if (item == null)
break;
Console.WriteLine(item);
}
}
async void Caller()
{
//同步获取任务列表
IEnumerable items=GetStuff();
//迭代完成任务
foreach(项目中的任务项异步)
{
//等待任务完成。我们需要等待
//在我们知道它是否是世界末日之前,它必须完成
//序列
SomeClass项=等待项异步;
//序列结束?
如果(项==null)
打破
控制台写入线(项目);
}
}
在这种情况下,
GetStuff
会立即返回一个可枚举项,其中可枚举项中的每个项都是一个任务,完成后将呈现一个SomeClass
对象。这种方法有一些缺陷。首先,枚举同步返回,所以在它返回时,我们实际上不知道结果中有多少行,这就是为什么我将其设置为无限序列。这是完全合法的,但也有一些副作用。我需要使用null
来表示任务无限序列中有用数据的结束。第二,您必须小心如何迭代它。您需要向前迭代,并且需要等待每一行,然后再迭代到下一行。您还必须仅在所有任务完成后处置迭代器,以便GC在使用完连接之前不会收集连接。由于这些原因,这不是一个安全的解决方案,我必须强调,我把它包括进来是为了帮助回答您的第二个问题。严格地说是异步迭代器的(或者有可能)根据我的经验,在SqlCommand
的上下文中,我注意到代码的同步版本远远优于它的async
对应版本。在速度和内存消耗方面
也许,对这一观察持保留态度,因为测试的范围仅限于我的机器和本地SQL Server实例
别误会,在适当的环境下,.NET环境中的范例非常简单、强大和有用。然而,经过很多努力,我不相信数据库访问是一个合适的用例。当然,除非您需要同时执行多个命令,在这种情况下,您可以简单地使用来同时发出命令
我更倾向于考虑以下因素:
- 使SQL工作单元保持小型、简单和可组合(即,使您的SQL执行“便宜”)
- 避免在SQL Server上执行可以向上游推送到应用程序级别的工作。排序就是一个很好的例子
- 最重要的是,按比例测试SQL代码并查看统计信息IO输出/执行计划。一个在10k记录下快速运行的查询,当有1M记录时,可能(也可能会)表现完全不同
IEnumerable<Task<SomeClass>> GetStuff()
{
using (SqlConnection conn = new SqlConnection(""))
{
using (SqlCommand cmd = new SqlCommand("", conn))
{
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
while (true)
yield return ReadItem(reader);
}
}
}
async Task<SomeClass> ReadItem(SqlDataReader reader)
{
if (await reader.ReadAsync())
{
// Create an instance of SomeClass based on row returned.
SomeClass someClass = null;
return someClass;
}
else
return null; // Mark end of sequence
}
async void Caller()
{
// Synchronously get a list of Tasks
IEnumerable<Task<SomeClass>> items = GetStuff();
// Iterate through the Tasks
foreach (Task<SomeClass> itemAsync in items)
{
// Wait for the task to complete. We need to wait for
// it to complete before we can know if it's the end of
// the sequence
SomeClass item = await itemAsync;
// End of sequence?
if (item == null)
break;
Console.WriteLine(item);
}
}