昨天的文章發表後,有兩位網友提到了DataContext是否要加using的議題。

我接觸LINQ to SQL是由Scott Gu的這幾篇文章開始入門的,在他的範例中沒有特別提到using,我也自始就忽略DataContext有實做IDispose這件事。雖然用using包住絕對有益無害(只要小心using中間過程如將DataContext傳到外部,要留意using結束後外部就不可再繼續叫用),但我倒認為DataContext裡的Connection應該不是一new DataContext就建立一條連線不放,直到Dispose()才結束釋出,而是Query時開啟連線,用完即關閉;Update時再重新開啟連線,用完再度關閉。如此【最後一刻開啟、用完立即關閉】的寫法,較符合珍貴資源應用的Best Practice;換句話說,在DataContext裡應該就己實踐了類似using (SqlConnection)的做法,盡可能讓連線時間最短化。不過,畢竟只是猜想,不如做個實驗觀察一下。

static void log(string msg)
{
    Console.WriteLine("{0:HH:mm:ss.fff} {1}", DateTime.Now, msg);
    System.Threading.Thread.Sleep(2000);
}
 
static void Main(string[] args)
{
    MyLabDataContext db = new MyLabDataContext();
    log("DataContext created");
    var q = from o in db.Players
            select o;
    log("LINQ defined");
    Console.WriteLine(q.Count());
    log("Get the row count");
    Player p = new Player()
    {
        UserName = "Darkthread",
        RegDate = new DateTime(2009, 1, 1),
        HiScore = 65536
    };
    db.Players.InsertOnSubmit(p);
    log("InsertOnSubmit");
    db.SubmitChanges();
    log("SubmitChanges");
    db.Dispose();
    log("Disposed");
    Console.Read();
}

在以上的程式範例中,每個動作間相隔兩秒鐘,並記錄發生時間,以便跟SQL Profiler記錄的時間對照。依我的推論,連線開啟關閉應該會發生在q.Count()與db.SubmitChanes()。

00:55:50.285 DataContext created
00:55:52.292 LINQ defined
0
00:55:54.730 Get the row count
00:55:56.736 InsertOnSubmit
00:55:58.850 SubmitChanges
00:56:00.850 Disposed

接著我們對照一下SQL Profiler看到的結果:

 

 
EventClassStartTimeEndTimeTextData
Audit Login2009-10-19 00:55:54.587NULL-- network protocol: TCP/IP
Audit Logout2009-10-19 00:55:54.5872009-10-19 00:55:54.647NULL
RPC:Completed2009-10-19 00:55:54.6472009-10-19 00:55:54.647exec sp_reset_connection
Audit Login2009-10-19 00:55:54.647NULL-- network protocol: TCP/IP
SQL:BatchCompleted2009-10-19 00:55:54.6472009-10-19 00:55:54.647SELECT COUNT(*) AS [value] FROM [dbo].[Player] AS [t0]
Audit Logout2009-10-19 00:55:54.6472009-10-19 00:55:58.720NULL
RPC:Completed2009-10-19 00:55:58.7202009-10-19 00:55:58.720exec sp_reset_connection
Audit Login2009-10-19 00:55:58.720NULL-- network protocol: TCP/IP
RPC:Completed2009-10-19 00:55:58.8102009-10-19 00:55:58.810exec sp_executesql N'INSERT INTO [dbo].[Player]([UserName], [RegDate], [HiScore]) VALUES (@p0, @p1, @p2)',N'@p0 nvarchar(10),@p1 datetime,@p2 int',@p0=N'Darkthread',@p1='2009-01-01 00:00:00',@p2=65536
Audit Logout2009-10-19 00:55:58.7202009-10-19 00:59:25.550NULL

我這麼解讀,00:55:50建立DataContext時並沒有開啟任何連線,q.Count()發生於00:55:54,此時有了第一次Audit Login。基於Connection Pooling的特性,每次SqlConnection.Open()時,會產生一個Audit Logout、exec sp_reset_connection再加一個Audit Login,而在此發生了兩次,第一次在00:55:54,第二次在00:55:58,剛好就是q.Count()及SubmitChanges()發生的時點。

最後一個Audit Logout則是出現在程式完全結束,Process終止,Connection Pool的連線真的被關閉時(00:59:25)。

由以上觀察,應可印證我的推論"LINQ to SQL裡DataContext內部對SqlConnection的處理原則為: 必要時才開啟,用後立即關閉"。進一步延伸,加上using應不會產生顯著影響,但對於實作IDispose的物件使用using絕對是良好的習慣。


Comments

# by Will 保哥

依我個人的經驗來說,在 DataContext 不利用 using 確保資源被釋放的情況下,的確是有機會導致資源耗盡的情況! 我們先前有寫過一支 Windows Service 程式,是長時間不中斷在執行的,結果不到兩個月的時間,客戶端就回報在 Event Log 中發現有幾次 OutOfMemory 的情況 (很少量, 但有! ),導致服務被終止,雖然可以在服務管理員中有「失敗恢復」的設定,但畢竟還是造成程式不穩定! 當我們改用 using 之後就沒再發生問題了。

# by Jeffrey

感謝保哥寶貴的經驗,為"加上using有利無害"再多添一筆證言。雖然以茶包射手的辦案標準來看,這個案例還不算為"using與OutOfMemory絕對相關"提供了"直接鐵證",除非再把using拿掉,確保相同的使用條件下再試一段時間。但要在正式環境長期使用觀察(故意改回可疑寫法等出錯,會被客戶殺死吧?),又要排除其他變因的干擾,工程浩大,代價高昂。我想在本例中,加上using免除疑慮的做法100%正確。我對DataContext在何種情況下會發生Connection資源未釋放的議題很感興趣,有空再來研究一下好了。再次感謝保哥提供的經驗分享!

# by Ark

挖丟災...

# by aliku

對有實做 IDisposed 加上 using 是個好的習慣. 感謝分享 !!

# by justfriend

你好,想請問【最後一刻開啟、用完立即關閉】的寫法和用connection pool的寫法,那種比較好?或是什麽情況下用【最後一刻開啟、用完立即關閉】,什麽情況下用connection pool呢?

# by Jeffrey

to justfriend, 在ADO.NET預設在做cn.Open()、cn.Close()時,就有動用到Connection Pool,用完立即關閉的連線會被放入Pool中,下次再開啟時會被再拿來重覆使 用,二者並非互斥的,而【最後一刻開啟、用完立即關閉】的對象其實就是Connection Pool裡的已開啟連線。

# by justfriend

原來是ado.net已處理掉connection pool的機制,謝謝你的回覆 ^^。

# by nancy

想請問黑暗大, 如果有 Cnn.close() 還需要Cnn.Dispose() 嗎? 相對的有 .Dispose還需要 .close 嗎? 它們兩者之間的關係是什麼呢? 我該怎麼去測試呢?

# by Jeffrey

to nancy, Cnn.Dispose()時會有內部的邏輯去視狀況呼叫Close()。用Dispose()可以確保不管發生任何意外,Cnn都一定會被結束。一般遇到要確保結束釋放資源的需求,建議用using (...) { } [http://msdn.microsoft.com/zh-tw/library/yh598w02.aspx ],就能保證{ }結束時一定Dispose(), 把資源釋放出來。 至於Cnn都包了using等於會呼叫Dispose()去呼叫Close(),還要不要寫Close()? 我個人認為寫出來語意較清楚,不是壞事。

# by tinydonut

conn = new SqlConnection(); try { conn.ConnectionString = "integrated security=SSPI;SERVER=YOUR_SERVER;DATABASE=YOUR_DB_NAME;Min Pool Size=5;Max Pool Size=60;Connect Timeout=2;"; // Notice Connection Timeout set to only two seconds! conn.Open(); } catch(Exception) { if (conn.State != ConnectionState.Closed) conn.Close(); conn.ConnectionString = "integrated security=SSPI;SERVER=YOUR_SERVER;DATABASE=YOUR_DB_NAME;Pooling=false;Connect Timeout=45;"; conn.Open();

# by nancy

非常感謝黑暗大的教導, 想再請問你一個問題, 如果之前有一個程式忘記關閉 conn.close(), 那DB要如何找出那些連線 並關閉呢? 或是DB本身有機制一天就關閉了?

# by Jeffrey

to nancy, 不同DB的管理策略不同,依我自己的經驗,SQL Server連線在Process結束時多半就會被強迫關閉,但ORACLE上有遇過機器都重開了,連線仍在DB端存活的經驗。 DB上有方法或指令可以列出目前有哪些連線、來源為何、在忙什麼? 只是不同的Server做法不盡相同,如果分工上有專職DBA,遇到這類問題我多半就"閃開! 讓專業的來" XD

# by 星寂

http://blog.darkthread.net/blogs/darkthreadtw/archive/2009/10/18/linq-to-oracle-hello-world.asp 這頁不見了~

# by Jeffrey

to 星寂,結果是href中aspx的x不見了,已校正,謝謝提醒。

Post a comment