非同步查詢與儲存

備註

僅限 EF6 及以上 版本 - 本頁討論的功能、API 等皆於 Entity Framework 6 中引入。 如果你使用的是較早期版本,部分或全部資訊可能不適用。

EF6 引入了支援非同步查詢與儲存的功能,使用 .NET 4.5 中引入的 async 和 await 關鍵字。 雖然並非所有應用程式都能從非同步中受益,但它可用於提升客戶端響應速度與伺服器可擴展性,尤其處理長期執行的網路或 I/O 任務。

什麼時候該真正使用非同步

本教學旨在以易於觀察非同步與同步程式執行差異的方式介紹非同步概念。 本攻略並非旨在說明非同步程式設計帶來好處的關鍵情境。

非同步程式設計主要著重於釋放目前的受管理執行緒(執行 .NET 程式碼的執行緒)以執行其他工作,同時等待不需要管理執行緒計算時間的操作。 例如,當資料庫引擎處理查詢時,.NET 程式碼無法完成任何事。

在用戶端應用程式(WinForms、WPF 等)中,目前執行緒可用來保持介面響應式,同時執行非同步操作。 在伺服器應用程式(ASP.NET 等)中,執行緒可用於處理其他進來請求——這能降低記憶體使用率及/或提升伺服器吞吐量。

在大多數應用中,使用非同步不會有明顯好處,甚至可能有害。 在決定前,先用測試、分析和常識來衡量非同步在你特定情境中的影響。

這裡有更多資源來了解非同步:

建立模型

我們將使用 Code First 工作流程 來建立模型並產生資料庫,但非同步功能將適用於所有 EF 模型,包括使用 EF Designer 建立的模型。

  • 建立一個主控台應用程式,並命名為 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 呼叫將新 部落格 儲存到資料庫的方法,然後從資料庫中擷取所有 部落格 並列印到 主控台。 之後,程式會寫下當日名言給 主控台

由於程式碼是同步的,執行程式時可以觀察到以下執行流程:

  1. SaveChanges 開始推送新 部落格 到資料庫
  2. SaveChanges 完成
  3. 查詢所有 部落格 的資訊會傳送到資料庫
  4. 查詢回傳與結果會寫入 主控台
  5. 今日名言寫給 Console

同步輸出  

 

讓它變成非同步

現在程式已經啟動並運行,我們可以開始使用新的 async 和 await 關鍵字。 我們對Program.cs做了以下調整

  1. 第2行:命名空間的 using 語句 System.Data.Entity 讓我們能存取 EF 非同步擴充方法。
  2. 第 4 行:命名空間的 using 語句 System.Threading.Tasks 允許我們使用 型 Task 別。
  3. 第 12 與 18 行:我們設定一個任務來監控 PerformSomeDatabaseOperations 的進度(第 12 行),然後在所有程式工作完成後封鎖程式執行,直到該任務完成為止(第 18 行)。
  4. 第 25 行:我們已將 PerformSomeDatabaseOperations 更新並標記為 async,並回傳 Task
  5. 第35行:我們現在正在呼叫 SaveChanges 的非同步版本,並等待其完成。
  6. 第 42 行:我們現在呼叫ToList的非同步版本,並等待其結果。

如需在命名空間中完整列出可用的擴充方法 System.Data.Entity ,請參考該 QueryableExtensions 類別。 你還需要在使用說明中補充 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);
                    }
                }
            }
        }
    }

現在程式碼是非同步的,我們可以觀察執行程式時不同的執行流程:

  1. SaveChanges 開始推送新 部落格 到資料庫
    一旦指令傳送到資料庫,目前管理執行緒就不需要更多的運算時間。 PerformDatabaseOperations方法會回傳(即使還沒執行完),Main 方法的程式流程也會繼續。
  2. 今日名言寫給 Console
    由於 Main 方法中已無更多工作可做,受管理的執行緒會在 Wait 呼叫時被阻塞,直到資料庫操作完成。 完成後,我們PerformDatabaseOperations的其餘部分將會執行。
  3. SaveChanges 完成
  4. 查詢所有 部落格 的資訊會傳送到資料庫
    再次,在查詢在資料庫中被處理時,受管理的執行緒可以自由執行其他工作。 但因為其他執行都已完成,執行緒會在等待呼叫時停止。
  5. 查詢回傳與結果會寫入 主控台

非同步輸出  

 

重點摘要

我們現在看到使用 EF 非同步方法是多麼容易。 雖然非同步的優點在簡單的主控台應用程式中可能不明顯,但這些策略同樣適用於長時間執行或網路受限活動可能阻塞應用程式,或導致大量執行緒增加記憶體佔用的情況。