异步查询和保存
注意
仅限 EF6 及更高版本 - 此页面中讨论的功能、API 等已引入实体框架 6。 如果使用的是早期版本,则部分或全部信息不适用。
EF6 使用 .NET 4.5 中引入的 async 和 await 关键字引入了对异步查询和保存的支持。 虽然并非所有应用程序都可以从异步编程中受益,但在处理长时间运行的任务、受网络或 I/O 限制的任务时,它可用于提高客户端响应能力和服务器可伸缩性。
何时真正使用异步编程
本演练旨在以一种易于观察异步和同步程序执行之间差异的方式介绍异步概念。 本演练并不打算说明异步编程带来好处的任何关键方案。
异步编程主要侧重于释放当前托管线程(运行 .NET 代码的线程)以执行其他工作,同时等待不需要托管线程中的任何计算时间的操作。 例如,当数据库引擎处理查询时,.NET 代码无事可做。
在客户端应用程序(WinForms、WPF 等)中,当前线程可用于在执行异步操作时保持 UI 的响应能力。 在服务器应用程序(ASP.NET 等)中,该线程可用于处理其他传入请求 - 这可以减少内存使用量和/或增加服务器的吞吐量。
在大多数应用程序中,使用异步编程并没有什么明显的好处,甚至可能会带来不利影响。 在提交异步编程之前,通过测试、分析和常识来衡量异步编程在特定方案中的影响。
下面提供了更多资源来帮助你了解异步编程:
创建模型
我们将使用 Code First 工作流来创建模型并生成数据库,但是异步功能适用于所有 EF 模型,包括使用 EF 设计器创建的模型。
- 创建一个控制台应用程序并将其命名为 AsyncDemo
- 添加 EntityFramework NuGet 包
- 在解决方案资源管理器中,右键单击 AsyncDemo 项目
- 选择“管理 NuGet 包…”
- 在“管理 NuGet 包”对话框中,选择“联机”选项卡,然后选择 EntityFramework 包
- 单击“安装”
- 添加具有以下实现的 Model.cs 类
using System.Collections.Generic;
using System.Data.Entity;
namespace AsyncDemo
{
public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
}
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public virtual List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public virtual Blog Blog { get; set; }
}
}
创建同步程序
现在有了 EF 模型,让我们编写一些代码,使用该模型执行一些数据访问。
- 将 Program.cs 的内容替换为以下代码
using System;
using System.Linq;
namespace AsyncDemo
{
class Program
{
static void Main(string[] args)
{
PerformDatabaseOperations();
Console.WriteLine("Quote of the day");
Console.WriteLine(" Don't worry about the world coming to an end today... ");
Console.WriteLine(" It's already tomorrow in Australia.");
Console.WriteLine();
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
public static void PerformDatabaseOperations()
{
using (var db = new BloggingContext())
{
// Create a new blog and save it
db.Blogs.Add(new Blog
{
Name = "Test Blog #" + (db.Blogs.Count() + 1)
});
Console.WriteLine("Calling SaveChanges.");
db.SaveChanges();
Console.WriteLine("SaveChanges completed.");
// Query for all blogs ordered by name
Console.WriteLine("Executing query.");
var blogs = (from b in db.Blogs
orderby b.Name
select b).ToList();
// Write all blogs out to Console
Console.WriteLine("Query completed with following results:");
foreach (var blog in blogs)
{
Console.WriteLine(" " + blog.Name);
}
}
}
}
}
此代码调用 PerformDatabaseOperations
方法,该方法将新的 Blog 保存到数据库,然后从数据库中检索所有 Blog 并将它们打印到控制台。 之后,程序将 Quote of the day 写入控制台。
由于代码是同步的,我们在运行程序时可以观察到以下执行流:
SaveChanges
开始将新的 Blog 推送到数据库SaveChanges
完成- 对所有 Blog 的查询发送到数据库
- 查询返回,结果写入控制台
- Quote of the day 写入控制台
使其成为异步程序
我们已经启动并运行程序,现在可以开始使用新的 async 和 await 关键字。 我们对 Program.cs 进行了以下更改
- 第 2 行:
System.Data.Entity
命名空间的 using 语句使我们能够访问 EF 异步扩展方法。 - 第 4 行:
System.Threading.Tasks
命名空间的 using 语句允许我们使用Task
类型。 - 第 12 行和第 18 行:我们正在捕获用于监视
PerformSomeDatabaseOperations
进度的任务(第 12 行),然后在程序的所有工作完成后阻止程序执行此任务(第 18 行)。 - 第 25 行:我们已更新
PerformSomeDatabaseOperations
,以标记为async
并返回Task
。 - 第 35 行:我们现在正在调用
SaveChanges
的异步版本并等待它完成。 - 第 42 行:我们现在正在调用
ToList
的异步版本并等待结果。
有关 System.Data.Entity
命名空间中可用扩展方法的完整列表,请参阅 QueryableExtensions
类。 你还需要在 using 语句中添加 using System.Data.Entity
。
using System;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
namespace AsyncDemo
{
class Program
{
static void Main(string[] args)
{
var task = PerformDatabaseOperations();
Console.WriteLine("Quote of the day");
Console.WriteLine(" Don't worry about the world coming to an end today... ");
Console.WriteLine(" It's already tomorrow in Australia.");
task.Wait();
Console.WriteLine();
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
public static async Task PerformDatabaseOperations()
{
using (var db = new BloggingContext())
{
// Create a new blog and save it
db.Blogs.Add(new Blog
{
Name = "Test Blog #" + (db.Blogs.Count() + 1)
});
Console.WriteLine("Calling SaveChanges.");
await db.SaveChangesAsync();
Console.WriteLine("SaveChanges completed.");
// Query for all blogs ordered by name
Console.WriteLine("Executing query.");
var blogs = await (from b in db.Blogs
orderby b.Name
select b).ToListAsync();
// Write all blogs out to Console
Console.WriteLine("Query completed with following results:");
foreach (var blog in blogs)
{
Console.WriteLine(" - " + blog.Name);
}
}
}
}
}
现在代码是异步的,我们可以在运行程序时观察到不同的执行流:
SaveChanges
开始将新的 Blog 推送到数据库
将命令发送到数据库后,当前托管线程上不再需要计算时间。PerformDatabaseOperations
方法返回(即使它尚未完成执行)并且 Main 方法中的程序流继续。- Quote of the day 写入控制台
由于无需在 Main 方法中执行其他操作,因此,托管线程将在Wait
调用上受阻,直到该数据库操作完成。 完成后,将执行PerformDatabaseOperations
的其余部分。 SaveChanges
完成- 对所有 Blog 的查询发送到数据库
同样,在数据库中处理查询时,托管线程可以自由执行其他操作。 由于所有其他执行都已完成,因此,线程将在 Wait 调用上停止。 - 查询返回,结果写入控制台
要点
我们现在知道了使用 EF 的异步方法是多么容易。 虽然异步方法的优势在简单的控制台应用中可能并不明显,但在长时间运行的活动或受网络限制的活动可能会阻塞应用程序或导致大量线程增加内存占用的情况下,可以应用这些相同的策略。