如何避免相同的 ConsoleApp 或 WinForm 同時間重複執行

前幾天在寫主控台程式 (Console Application) 時突然想到一個避免程式在同時間重複執行的機制,在噗浪發問與自行研究過後發現最彈性的實做方式是利用 .NET 內建的 Mutex 類別進行實做,幾乎任何情況下都能輕易實做程式不重複執行的目的,包括單機環境與多人使用的伺服器環境。

Mutex 類別有兩種用法:

  1. 不具名 Mutex 用法:在單一程序內實做 Mutex 機制 [ 可參考 MSDN 的 Mutex 程式範例 ]
  2. 具名的 Mutex 用法:在整台主機系統內實做 Mutex 機制

不具名 Mutex 用法較常實做在同一個程序內為了確保資源僅被單一執行緒使用,才會利用不具名 Mutex 的物件進行互斥處理。

其中具名的 Mutex 用法還包括兩種使用範圍:

  1. 在同壹台主機相同使用者的範圍內進行 Mutex 互斥 ( 採用 Local\ 前置詞 )
  2. 在同壹台主機所有使用者的範圍內進行 Mutex 互斥( 採用 Global\ 前置詞 )

我在實驗的時候設定了一個較複雜的情境,描述如下:

  • 要在同一台主機內,不管有多少 User 登入電腦執行同一支程式,都要進行互斥(Mutex)處理
  • 程式若是在相同目錄下才需要進行互斥處理,如果程式複製到不同的目錄就不需要進行互斥處理
  • 由於該 Console 程式有傳入參數,當輸入參數完全相同時必須要進行互斥處理

依據上述需求,我實做出以下程式:

static void Main(string[] args)
{
  bool is_createdNew1;
  bool is_createdNew2;
  Mutex mu1 = null;
  Mutex mu2 = null;

  try
  {
    #region 檢查程式是否重複執行

    // 第一關:在同目錄執行相同程式的情況下不允許重複執行
    string mutexName1 = Process.GetCurrentProcess().MainModule.FileName
                .Replace(Path.DirectorySeparatorChar, '_');
    mu1 = new Mutex(true, "Global\\" + mutexName1, out is_createdNew1);
    if (!is_createdNew1)
      return;

    // 第二關:在完全相同的傳入參數下不允許重複執行,避免數據重複計算
    string mutexName2 = "Args_" + String.Join("_", args)
							.Replace(Path.DirectorySeparatorChar, '_');
    mu2 = new Mutex(true, "Global\\" + mutexName2, out is_createdNew2);
    if (!is_createdNew2)
      return;

    #endregion

    DoSomething();
  }
  catch (Exception ex)
  {
    throw ex;
  }
  finally
  {
  }
}

如果你僅需限制互斥的範圍只需在「同使用者」的範圍內,只需將 Mutex 名稱的 Global\ 改成 Local\ 即可。如果你還有更多其他條件需判斷,只需要多建立幾可 Mutex 進行判斷,所以這樣的設計方式十分彈性,可以自行組合出多種變化。

眼尖的讀者可能會發現從上述程式在設定 Mutex 名稱時多做了一些手腳,也就是必須將所有反斜線符號 ( \ ) 都換成底線符號 ( _ ),因為反斜線 ( \ ) 符號在 Mutex 中有特殊意義,另外從 MSDN 文件中也發現 Mutex 名稱長度也不得大於 260 個字元,條列整理如下:

Mutex 命名限制

  • Mutex 名稱長度不得大於 260 個字元
  • Mutex 名稱不能使用反斜線 ( \ ) 符號

其他與 Mutex 類別有關的細節請各位細讀 MSDN 文件。

相關連結

  

此文章由 will 發表於 2009/10/23 下午 11:13:01

永久連結 | 評論 (2) | 此文章的RSSRSS comment feed |

分類: C# | .Net

標籤: , , , ,

收藏:

如何確保寫入檔案時「檔案名稱」與「目錄名稱」正確無誤

當要在檔案系統 (Filesystem) 寫入檔案時必須指定完整的路徑與檔名,但有時「檔案名稱」需透過程式指定時就有可能發生「檔案名稱無效」的錯誤,這時就需要透過程式過濾掉一些無效字元,才能讓成功建立檔案。

網路上資料雜亂,使用錯誤的方法也很多,所以我通常在網路上搜尋資料時通常不敢亂用,一般都會多方資料蒐集並且自行測試驗證過後才敢放心使用。

我覺得用 .NET Framework 寫程式最忌諱 重新發明輪子 (Reinventing the wheel),正確的觀念應該是能用 .NET Framework 做出來的功能,千萬不要自行實做,這樣才能確保你的 .NET 程式在未來更有可攜性(portability) [ 例如日後程式需要移到 Linux 平台實做 ]。

像是很常用的 System.IO.Path 類別就定義了兩個非常實用的方法(method),分別是以下兩個:

透過這兩個方法就可以得知在目前平台下 [如: Windows] 對於「目錄名」與「檔名」的被限制一定不能使用的字元有哪些,除此之外的字元都可以當作檔名或目錄名,例如中文或其他 Unicode 字元。

除了被限制的字元外,在檔名最後面不能出現小數點,也就是你可以沒有副檔名,但你不能用「空白」的副檔名,因此這類情況也必須將結尾的小數點全部刪除才行。

最後,我總結出以下程式:

static string MakeFilenameValid(string filename)
{
    if (filename == null)
        throw new ArgumentNullException();

    if (filename.EndsWith("."))
        filename = Regex.Replace(filename, @"\.+$", "");

    if (filename.Length == 0)
        throw new ArgumentException();

    if (filename.Length > 245)
        throw new PathTooLongException();

    foreach (char c in System.IO.Path.GetInvalidFileNameChars())
    {   
        filename = filename.Replace(c, '_');
    }
        
    return filename;
}

static string MakeFoldernameValid(string foldername)
{
    if (foldername == null)
        throw new ArgumentNullException();

    if (foldername.EndsWith("."))
        foldername = Regex.Replace(foldername, @"\.+$", "");

    if (foldername.Length == 0)
        throw new ArgumentException();
    
    if (foldername.Length > 245)
        throw new PathTooLongException();
    
    foreach (char c in System.IO.Path.GetInvalidPathChars())
    {
        foldername = foldername.Replace(c, '_');
    }

    return foldername;
}

除了檔案系統外,在各種不同的應用系統或平台對於目錄與檔名的限制也不一定一樣,例如 SharePoint 就對目錄名與檔名的限制更多,相關資訊可參考以下相關連結。

相關連結

  

此文章由 will 發表於 2009/10/19 上午 11:59:00

永久連結 | 評論 (8) | 此文章的RSSRSS comment feed |

分類: .Net | C#

標籤: , , , , , , ,

收藏:

關於 C# 的 using 陳述式在實務應用上的基本觀念

之前偶有機會看到 MSDN 論壇上有人提到關於使用 using 陳述式的錯誤觀念 (看到不止一次),由於自己也經常在用,不知道原來有人會誤以為使用 using 會將所有例外狀況 (Exception) 給吃掉,但事實上並非如此,請讓我娓娓道來。

using 有兩種,相信許多人經常會用到:

1. 一種是作為「指示詞」,可用來建立命名空間的別名(Alias),或用來匯入在其他命名空間中定義的型別
2. 另一種是「陳述式」,可用來定義一個範圍,該範圍內的物件會在此範圍結尾處執行 Dispose 方法。

第一種:using 指示詞

標準用法:

using System.Text;

別名用法:

using Models = MvcApplication.Models;

第二種:using 陳述式 ( 也是我今天想強調的部分 )

using 陳述式是一個非常基本的 C# 語法,相信在實務開發上經常會使用到,如果沒使用過的人可能代表你對 .NET 的記憶體管理的 Sense 不太夠,很有可能寫出資源耗盡的 .NET 程式碼。

雖然 .NET 有內建強大的記憶體管理機制(GC),但開發人員還是不能完全依賴 .NET 來處理一些無法釋放的資源,例如:Handles, Unmanaged Resources, …。

而使用 using 最主要的目的是為了讓物件建立的同時能確保該物件所佔用的資源一定會被完整釋放,如果沒有釋放這些無法自動釋放的資源,就很有可能讓 .NET 應用程式發生 資源耗盡 (Resource Exhausted) 的狀況。

從 MSDN 上節錄一段範例程式如下:

using (System.IO.StreamReader sr = 
         new System.IO.StreamReader(@"C:\test.txt"))
{
    string s = null;
    while((s = sr.ReadLine()) != null)
    {
        Console.WriteLine(s);
    }
}

使用 using 陳述式有一個最基本的條件,就是該物件必須有實做 IDisposable 介面,才能確保在 using 的結尾數時自動執行 Dispose() 方法

使用 using 陳述式的另一個強烈建議的條件,就是該物件被建立的語法必須寫在 using 子句中,否則物件很有可能在被釋放後還有其他物件存取的情況,進而引發例外狀況!

有些人認為 using 陳述式會自動被翻譯成以下程式碼:(錯誤範例

{
System.IO.StreamReader sr = new System.IO.StreamReader(@"C:\test.txt");
try
{
string s = null;
while((s = sr.ReadLine()) != null)
{
Console.WriteLine(s);
}
}
catch
{
}
finally
{
if (sr != null)
((IDisposable)sr).Dispose();
}
}

但事實上,應該不包含 catch 的區段才是!正確的語法應該如下:

{
	System.IO.StreamReader sr = new System.IO.StreamReader(@"C:\test.txt");
	try
	{
	  string s = null;
	  while((s = sr.ReadLine()) != null)
	  {
		  Console.WriteLine(s);
	  }
	}
	finally
	{
	  if (sr != null)
		((IDisposable)sr).Dispose();
	}
}

也就是在使用 using 陳述式時,雖然有 tryfinally,但並不包含 catch 的成分

換句話說,使用 using 陳述式並不會幫你捕捉例外狀況!!

所以就算你用 using 在程式中,若要處理 try-catch 的狀況還是要自行撰寫,否則就會遇到非預期的例外狀況而導致應用程式掛掉,這點必須特別注意!

相關連結

  

此文章由 will 發表於 2009/10/12 下午 10:27:25

永久連結 | 評論 (10) | 此文章的RSSRSS comment feed |

分類: C# | .Net | Tips

標籤: , , ,

收藏:

解決 XmlWriter 輸出的 XML 無法正確設定 Encoding 的問題

這問題我遇到第二次了,第一次遇到時濛濛懂懂的解決了。但不知為何越來越有種不想讓自己迷迷糊糊過一生的感覺,所以拼了命也要將模糊的地帶給釐清,於是又花了好幾個鐘頭把來龍去脈給釐清,避免日後又再度遇到同樣的問題。這是個關於透過 XmlWriter 撰寫 XML 的問題,不管我如何設定 Encoding 編碼都無法改變輸出的編碼,經過一番努力辯證之後才徹底釐清觀念,以後也不會再混淆了。

先準備 XmlWriterSettings 類別定義輸出 XML 的排版格式與編碼,其中包括文字編碼的 Encoding 屬性:

XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.OmitXmlDeclaration = false;
settings.NewLineOnAttributes = true;
settings.Encoding = Encoding.GetEncoding("big5");

但是這裡的 XmlWriterSettings.Encoding 屬性根本就是個幌子,不管設定什麼 Encoding 輸出的結果一律都是 UTF-16 編碼。

接著,我們繼續把 XmlWriter 的程式碼寫完如下:

StringBuilder sb = new StringBuilder();
XmlWriter writer = XmlWriter.Create(sb, settings);

writer.WriteStartElement("root");
writer.WriteStartElement("person");
writer.WriteElementString("name", "游錫堃、喆喆創意");
writer.WriteEndElement();
writer.WriteEndElement();
writer.Flush();

Response.Write(sb.ToString());

輸出的結果卻是如下:

<?xml version="1.0" encoding="utf-16"?>
<root>
<person>
<name>游錫堃、喆喆創意</name>
</person>
</root>

原本我們預期的 encoding 不是 big5 嗎?為什麼總是輸出 utf-16 呢?

然後我換個方式寫,將最後得到的 StringBuilder 輸出成字串,並轉成 XmlDocument 物件處理,強迫將輸出編碼修改:

XmlDocument xdoc = new XmlDocument();
xdoc.LoadXml(sb.ToString());

XmlDeclaration decl = (XmlDeclaration)xdoc.FirstChild;
decl.Encoding = "big5";

Response.Write(xdoc.OuterXml);

這是換湯不換藥,因為事實上輸出的編碼還是 UTF-16,只是 <?xml version="1.0" encoding="big5"?> 的 encoding 部分被硬改成 big5 而已,而這當然是錯誤示範,小朋友可千萬不要學,叔叔是有練過的。

這時我又再換一種寫法,改用 Stream 的型別傳入就能夠正確處理 Encoding 屬性了:

using (Stream fs = File.Open("a.xml", FileMode.CreateNew))
{
    XmlWriter writer = XmlWriter.Create(fs, settings);

    writer.WriteStartElement("root");
    writer.WriteStartElement("person");
    writer.WriteElementString("name", "游錫堃、喆喆創意");
    writer.WriteEndElement();
    writer.WriteEndElement();
    writer.Flush();

    fs.Close();
}

當然,如果我用 Response.OutputStream 直接輸出到網頁,當然也是可行的:

XmlWriter writer = XmlWriter.Create(Response.OutputStream, settings);

writer.WriteStartElement("root");
writer.WriteStartElement("person");
writer.WriteElementString("name", "游錫堃、喆喆創意");
writer.WriteEndElement();
writer.WriteEndElement();
writer.Flush();

輸出的結果正如預期的是:

<?xml version="1.0" encoding="big5"?>
<root>
<person>
<name>游錫&#x5803;、&#x5586;&#x5586;創意</name>
</person>
</root>

以上這兩個成功案例,都是用 Stream 型別傳入才成功的,但一個是直接輸出成檔案、一個是直接輸出到網頁(Response.OutputStream),如果我們需要將 XmlWriter 寫入的資料暫時放在記憶體中的話,就必須利用 MemoryStream 的方式當成緩衝區(buffer)。

我首先做了以下嘗試 (錯誤示範):

Encoding enc = Encoding.GetEncoding("big5");

XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.OmitXmlDeclaration = false;
settings.NewLineOnAttributes = true;
settings.Encoding = enc;

StringBuilder sb = new StringBuilder();

MemoryStream ms = new MemoryStream();
StreamWriter sw = new StreamWriter(ms);

XmlWriter writer = XmlWriter.Create(sw, settings);

writer.WriteStartElement("root");
writer.WriteStartElement("person");
writer.WriteElementString("name", "游錫堃、喆喆創意");
writer.WriteEndElement();
writer.WriteEndElement();
writer.Flush();

string result = enc.GetString(ms.ToArray());

Response.ContentType = "text/xml";
Response.Write(result);

但是輸出的結果卻是亂碼:

<?xml version="1.0" encoding="utf-8"?>
<root>
<person>
<name>皜賊?s€????/name>
</person>
</root>

從第一行看到就可以知道,其實 XmlWriter 還是以 UTF-8 當成處理所有 XmlWriter 內部處理的字元,所以才無法被 enc.GetString() 方法轉換成正確的 Big5 文字。

最後,我試著在 StreamWriter 加上 Encoding 參數 (如下 enc 部分),就可以正常處理所有 Big5 的字元,不過卻無法正確處理「非 Big5 字集」的字元。

StreamWriter sw = new StreamWriter(ms, enc);

輸出的結果是:

<?xml version="1.0" encoding="big5"?>
<root>
<person>
<name>游錫?、??創意</name>
</person>
</root>

到最後,我才發現我被我自己先前的程式碼給「制約」了,原來我一直在用 StreamWriter 在過濾所有應該被寫入 Stream 的文字,這反而限制了所有應該被正常處理的文字,所以才會導致有部分非字集內的文字沒有正常被轉成 Entities 的形式。

我最後把 StreamWriter 這一行給抽掉,所有的編碼的問題就解決了。以下是終極正確版:

Encoding enc = Encoding.GetEncoding("big5");

XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.OmitXmlDeclaration = false;
settings.NewLineOnAttributes = true;
settings.Encoding = enc;

MemoryStream ms = new MemoryStream();

XmlWriter writer = XmlWriter.Create(ms, settings);

writer.WriteStartElement("root");
writer.WriteStartElement("person");
writer.WriteElementString("name", "游錫堃、喆喆創意");
writer.WriteEndElement();
writer.WriteEndElement();
writer.Flush();

string result = enc.GetString(ms.ToArray());

Response.ContentType = "text/xml";
Response.Write(result);

心得結論

相關連結

  

此文章由 will 發表於 2009/10/3 上午 12:51:17

永久連結 | 評論 (4) | 此文章的RSSRSS comment feed |

分類: .Net | C#

標籤: , , , ,

收藏:

如何利用「自訂例外狀況」處理無法繼續執行的錯誤

try-catch 幾乎是每天必碰的程式碼,新手程式設計師應該很容易瞭解 try-catch 該怎麼使用,但不見得能深入思考「為什麼」要有 例外狀況(Exception) 的存在,存在的理由很多,但我個人認為最重要也最單純的理由是『例外狀況試圖不讓你的程式繼續執行下去』,這聽起來像是個廢話,但或許有人沒有認真的想過這個問題,而這樣的一個理由在面對日常程式開發上又有什麼重大的影響呢?

程序導向的程式開發架構下,錯誤處理都是靠 if-then-else 等於法,有時後會讓程式又臭又長變的難以維護也難以閱讀,到了物件導向程式設計的世界裡,有時也會難逃這種狀況,畢竟對人腦的思路來說「物件導向」一直都是「不人性化」的,我們學習物件導向技術就是為了克服人性,尋求一個更有結構、更有效率、更易於管理維護的解決方案。

在不熟悉物件導向程式設計時,通常都會用好多層的 if-then-else 處理各種例外狀況,為的不就是『當錯誤發生時不要執行到自己覺得不應該執行的程式碼』嗎?如果你今天寫程式的過程中,研判物件執行到某種狀態「有可能」造成程式發生不穩定的情況時,你就應該不要再繼續執行下去,否則一錯再錯的情況下,就會有更多狀況不可遇測了,這時你可以單純的 return value (也許是 null 之類的),或你也可以丟出一個例外狀況(Exception)

丟出例外狀況單純的 return value 有些不太一樣的地方,也就在於「例外狀況」可以包含比原本從方法(method) 或 函式(function) 「回傳資料」時有更多的資訊可以傳遞給呼叫者(Caller),這也是使用「例外狀況」最大的好處

相對的 Exception 最大的壞處也是相同的地方,因為越多的資訊傳遞帶來的卻是記憶體耗用、CPU 運算資源耗用等負面影響,所以當一個物件「有可能」頻繁的發生 Exception 時,這時就必須考慮運用其他的方式或設計範式(Design Pattern)來處理例外狀況帶來的效能問題,建議可以參考閱讀 MSDN 文章的【例外狀況和效能 / Exceptions and Performance】章節。

舉一個最常見的例子:Int32 ( int )

.NET 1.1 版的 Int32 成員中有個常用的 靜態方法(method) 叫 Int32.Parse 方法,這個方法相信大家也很常用,他可以傳入一個字串(System.String),解析後轉換成 Int32 型別,當轉換失敗時就會丟出一個 FormatException,但字串的型態何其多,很容易就會傳入「非數字」的字串,這時就「有可能」造成頻繁的發生例外狀況並造成效能衝擊。

從 .NET 2.0 開始,Int32 成員就多了一個 Int32.TryParse 靜態方法,主要的目的就是讓呼叫者不會頻繁的接收到過多且不需要的例外狀況,也徹底解決例外狀況帶來的效能問題。

前面都是講例外狀況的基本概念,終於要進入正題了:自訂例外狀況

在設計自訂例外狀況時建議可參考 .NET Framework 開發人員手冊設計自訂例外狀況 章節,依照上述的方針進行設計有助於您寫出正確且有用的例外型別。

標準的自訂例外型別範本如下 ( 參照自 MSDN 的 設計自訂例外狀況 章節 ):

public class NewException : BaseException, ISerializable {
public NewException() {
// Add implementation.
}
public NewException(string message) {
// Add implementation.
}
public NewException(string message, Exception inner) {
// Add implementation.
}
// This constructor is needed for serialization.
protected NewException(SerializationInfo info, StreamingContext context) {
// Add implementation.
}
}

我另外提供一個我自己寫過的自訂例外型別範例,這是一個當「使用者不存在」時所使用的例外狀況,當登入作業無法完成時,基本上不能再讓程式執行下去,所以我就丟出一個例外中斷程式執行。但事實上我在使用時並非讓應用程式因為例外狀況而自動關閉,而是用 try-catch 取得完整的例外資訊後顯示適當的錯誤訊息,範例程式如下:

class UserNotFoundException : Exception, ISerializable
{
public UserNotFoundException()
: base("使用者不存在") { }
public UserNotFoundException(string message)
: base(message) { }
public UserNotFoundException(string message, Exception inner)
: base(message, inner) { }
protected UserNotFoundException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}

在第一個建構子帶入一個預設的錯誤訊息,也就是當執行到下列程式時,由呼叫者取得的例外資訊就會包含這個例外型別提供的預設錯誤訊息:

throw new UserNotFoundException();

實際上 Exception 型別可以攜帶的資料還有更多,在 Exception 成員就可以看到其他內建的屬性,像是 HelpLink 可以儲存這個例外狀況相關聯的連結,Source 可以儲存造成錯誤的應用程式或物件名稱,InnerException 可以儲存造成這個例外的例外物件,Data 屬性型別為 IDictionary,可以儲存任意任何提供關於此例外狀況的額外使用者定義資訊。

例外狀況有這麼多的資訊可以傳遞,相信在程式開發上會有非常大的幫助,但也要小心隨之而來的效能衝擊,只要妥善掌握例外狀況開發的原則相信能夠寫出較合理的例外狀況處理程式。

相關連結

  

此文章由 will 發表於 2009/9/30 下午 12:31:07

永久連結 | 評論 (2) | 此文章的RSSRSS comment feed |

分類: .Net | C#

標籤: , , ,

收藏: