我正在使用 Entity Framework 6 处理一些 Web API 的东西,我的控制器方法之一是“Get All”,它希望从我的数据库中接收表的内容作为 IQueryable<Entity>
。在我的存储库中,我想知道是否有任何有利的理由来异步执行此操作,因为我不熟悉将 EF 与异步一起使用。
基本上它归结为
public async Task<IQueryable<URL>> GetAllUrlsAsync()
{
var urls = await context.Urls.ToListAsync();
return urls.AsQueryable();
}
对比
public IQueryable<URL> GetAllUrls()
{
return context.Urls.AsQueryable();
}
异步版本是否真的会在这里产生性能优势,或者我是否会通过首先投影到列表(使用异步提醒你)然后去 IQueryable 来产生不必要的开销?
问题似乎是您误解了异步/等待如何与实体框架一起工作。
关于实体框架
那么,让我们看看这段代码:
public IQueryable<URL> GetAllUrls()
{
return context.Urls.AsQueryable();
}
及其用法示例:
repo.GetAllUrls().Where(u => <condition>).Take(10).ToList()
那里会发生什么?
我们正在使用 repo.GetAllUrls() 获取 IQueryable 对象(尚未访问数据库) 我们使用 .Where(u =>
好的,我们来看第一个代码:
public async Task<IQueryable<URL>> GetAllUrlsAsync()
{
var urls = await context.Urls.ToListAsync();
return urls.AsQueryable();
}
使用相同的使用示例,我们得到:
我们正在使用 await context.Urls.ToListAsync(); 将存储在数据库中的所有十亿个 url 加载到内存中。我们遇到了内存溢出。杀死服务器的正确方法
关于异步/等待
为什么首选使用 async/await?让我们看一下这段代码:
var stuff1 = repo.GetStuff1ForUser(userId);
var stuff2 = repo.GetStuff2ForUser(userId);
return View(new Model(stuff1, stuff2));
这里会发生什么?
从第 1 行开始 var stuff1 = ... 我们向 sql server 发送请求,要求我们为 userId 获取一些 stuff1 我们等待(当前线程被阻塞) 我们等待(当前线程被阻塞)..... Sql server 发送到我们响应我们移动到第 2 行 var stuff2 = ... 我们向 sql server 发送请求,我们希望为 userId 获取一些 stuff2 我们等待(当前线程被阻塞)然后再次..... Sql server 向我们发送响应我们渲染视图
所以让我们看一下它的异步版本:
var stuff1Task = repo.GetStuff1ForUserAsync(userId);
var stuff2Task = repo.GetStuff2ForUserAsync(userId);
await Task.WhenAll(stuff1Task, stuff2Task);
return View(new Model(stuff1Task.Result, stuff2Task.Result));
这里会发生什么?
我们向 sql server 发送请求以获取 stuff1(第 1 行) 我们向 sql server 发送请求以获取 stuff2(第 2 行)我们等待 sql server 的响应,但当前线程没有被阻塞,他可以处理来自其他用户的查询 我们渲染视图
正确的方法
这里的代码很好:
using System.Data.Entity;
public IQueryable<URL> GetAllUrls()
{
return context.Urls.AsQueryable();
}
public async Task<List<URL>> GetAllUrlsByUser(int userId) {
return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();
}
请注意,您必须添加 using System.Data.Entity
才能将方法 ToListAsync()
用于 IQueryable。
请注意,如果您不需要过滤和分页等内容,则无需使用 IQueryable
。您可以只使用 await context.Urls.ToListAsync()
并使用具体化的 List<Url>
。
您发布的第一个版本的示例存在巨大差异:
var urls = await context.Urls.ToListAsync();
这不好,它基本上执行 select * from table
,将所有结果返回到内存中,然后将 where
应用于内存集合中的结果,而不是对数据库执行 select * from table where...
。
在将查询应用于 IQueryable
之前,第二种方法实际上不会命中数据库(可能通过 linq .Where().Select()
样式操作,它只会返回与查询匹配的 db 值。
如果您的示例具有可比性,则 async
版本的每个请求通常会稍微慢一些,因为编译器生成的状态机中有更多开销以允许 async
功能。
然而,主要区别(和好处)是 async
版本允许更多并发请求,因为它在等待 IO 完成时不会阻塞处理线程(数据库查询、文件访问、Web 请求等)。
长话短说,
IQueryable
是为了推迟 RUN 过程而设计的,首先结合其他 IQueryable
表达式构建表达式,然后将表达式作为一个整体进行解释和运行。
但是 ToList()
方法 (或类似的几种方法),需要立即“按原样”运行表达式。
您的第一个方法 (GetAllUrlsAsync
) 将立即运行,因为它是 IQueryable
后跟 ToListAsync()
方法.因此它会立即运行(异步),并返回一堆 IEnumerable
。
同时,您的第二个方法 (GetAllUrls
) 不会运行。相反,它返回一个表达式,并且此方法的 CALLER 负责运行该表达式。
GetAllUrlsByUser
方法中的结果集进行操作,因此您不需要使其异步。只需返回任务并为自己保存一个不必要的状态机,以免编译器生成。async
和await
。让调用者await
它。当您在此阶段等待调用return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();
时,您在反编译程序集并查看 IL 时创建了一个额外的异步包装器。