CODE-Convert Text to PDF in C#

透過程式直接將Text內容轉換成PDF的程式範例,寫來給其他組同事做為系統整合模組開發參考,順便PO文備忘。

要在.NET轉PDF,當然少不了大家都說讚的iTextSharp,程式很簡單,我還順手加了一個遇到"\f" (0x0C) Form Feed符號就強制換新頁的功能。

<%@ Page Language="C#" %>
<%@ Import Namespace="iTextSharp.text" %>
<%@ Import Namespace="iTextSharp.text.pdf" %>
<%@ Import Namespace="System.IO" %>
<script runat="server">
    protected void btnConvert_Click(object sender, EventArgs e)
    {
//REF: http://www.codeproject.com/KB/graphics/iTextSharpTutorial.aspx
        Document doc = new Document(PageSize.A4.Rotate());
        
        using (MemoryStream ms = new MemoryStream())
        {
 
            try
            {
                PdfWriter.GetInstance(doc, ms);
                doc.Open();
//中文字型問題REF 
//http://renjin.blogspot.com/2009/01/using-chinese-fonts-in-itextsharp.html
                string fontPath = 
                    Environment.GetFolderPath(
                    Environment.SpecialFolder.System) +
                    @"\..\Fonts\kaiu.ttf";
                BaseFont bfChinese = BaseFont.CreateFont(
                    fontPath,
                    BaseFont.IDENTITY_H, //橫式中文
                    BaseFont.NOT_EMBEDDED
                );
                Font fontChinese = new Font(bfChinese, 8f, Font.NORMAL);
                
                StringReader sr = new StringReader(txtInput.Text);
                string line = null;
                while ((line = sr.ReadLine()) != null)
                {
                    string[] p = line.Split('\f');
                    foreach (string s in p)
                    {
                        doc.Add(new Paragraph(s, fontChinese));
                        if (p.Length > 1) //表示有換頁符號
                            doc.NewPage();
                    }
                }
            }
            catch (DocumentException de)
            {
                Response.Write(de.Message);
                Response.End();
            }
            catch (IOException ioe)
            {
                Response.Write(ioe.Message);
                Response.End();
            }
            doc.Close();
            //Output PDF
            Response.Clear();
            Response.ContentType = "application/octet-stream";
            Response.AddHeader("content-disposition", 
                "attachment;filename=output.pdf");
            Response.BinaryWrite(ms.ToArray());
            Response.End();
        }
                    
    }
</script>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Text to PDF demo</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <asp:TextBox ID="txtInput" runat="server" Height="600px" TextMode="MultiLine" 
            Width="800px"></asp:TextBox>
    <br /><asp:Button ID="btnConvert" runat="server" onclick="btnConvert_Click" 
            Text="Convert to PDF" />
    </div>
    </form>
</body>
</html>
CODE-SetTimeout/ClearTimeout in C#

正在從事以休閒為目的Coding活動時,忽然有個衝動想在C#中也用一下Javascript裡常用的setTimeout/clearTimeout。

setTimeout說穿了就是透過另一條Thread執行程式產生非同步效果,用.NET實作是小菜一碟,而我想挑戰的是如何用最簡潔的方法實作出來。

剛好這陣子陸續玩過Action<T> and Func<T>Closure in C#,加上研究Parallel.For()時被迫反覆寫了十來次,現在已經練就信手就可掰出一段Thread配Lambda範例的境界。這個題目拿來作為隨堂考試再適合也不過了。

在Action加Lambda加Closure的加持之下,只要不到30行程式就可以在C#中使用SetTimeout/ClearTimeout囉~~~ C#真是簡潔有力的好語言呀!

先看程式碼,後面再提一下重點:

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Threading;
 
namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void Form1_Load(object sender, EventArgs e)
        {
            //No UI thread issue, use SetTimeout(cb, delay)
            SetTimeout(() =>
            {
                MessageBox.Show("First");
            }, 2000);
            SetTimeout(() =>
            {
                listBox1.Items.Add("Second");
            }, 4000, this);
            Guid hnd = SetTimeout(() =>
            {
                listBox1.Items.Add("Third");
            }, 6000, this);
            SetTimeout(() =>
            {
                listBox1.Items.Add("Forth");
            }, 8000, this);
            ClearTimeout(hnd);
        }
 
        #region SetTimeout/ClearTimeout Simulation
        //Dictionary for running setTimeout
        static Dictionary<Guid, Thread> _setTimeoutHandles =
            new Dictionary<Guid, Thread>();
        //SetTimeout for no UI Thread issue
        static Guid SetTimeout(Action cb, int delay)
        {
            return SetTimeout(cb, delay, null);
        }
        //Javascript-style SetTimeout function
        //remember to set uiForm argument when there cb is trying
        //to change UI controls in window form
        //it will return a GUID as handle for cancelling
        static Guid SetTimeout(Action cb, int delay, Form uiForm)
        {
            Guid g = Guid.NewGuid();
            Thread t = new Thread(() =>
            {
                Thread.Sleep(delay);
                _setTimeoutHandles.Remove(g);
                if (uiForm != null)
                    //use Invoke() to avoid threading issue
                    //Ref: http://tinyurl.com/yjckzhz
                    uiForm.Invoke(cb);
                else
                    cb();
            });
            _setTimeoutHandles.Add(g, t);
            t.Start();
            return g;
        }
        //Javascript-style ClearTimeout
        static void ClearTimeout(Guid g)
        {
            if (!_setTimeoutHandles.ContainsKey(g))
                return;
            _setTimeoutHandles[g].Abort();
            _setTimeoutHandles.Remove(g);
        }
        #endregion
    }
}

重點補充:

  1. SetTimeout的原理是呼叫端傳入Action參數及延遲微秒(ms)數後,立即新增一條Thread執行"先Thread.Delay()指定時間長度再呼叫所傳入的Action",並將控制權交回呼叫端,邏輯十分單純。
  2. 由於SetTimeout傳入要延遲執行的Action程式實際上會由另一條Thread執行,若在該邏輯中變動Window Form上的元素,就會違背不可透過非UI Thread去更動UI元素的規則。因此,我們再多增加一個Form參數,當有需要時,透過Form.Invoke()間接執行才可避開UI Thread限制。
  3. 要能ClearTimeout,就必須保留Thread變數,必要時將其Abort()掉。
    我用了一個Diction<Guid, Thread>來保存Thread,SetTimeout時Dictionary.Add()並傳回一個GUID,ClearTimeout時可憑該GUID去中止尚未執行的排程。
  4. 理論上,ClearTimeout只能中止"仍在等待執行的工作",因此Thread.Delay一結束時,就立刻Dictionary.Remove(GUID) (這裡剛好展現了Closure的美妙之處),不再允許ClearTimeout,否則程式都跑了一半還胡亂Thread.Abort()會出人命的。

寫完再回頭看,短短30行Code還真扯到不少進階的東西(Action, Closure, Threading),看懂的人C# Coding能力應該都在中階以上了吧!(純個人意見,勿戰) 對這一段還不熟的人可以參考以前PO過的幾篇文章當起頭,但我想還需要參考官方文件、網路範例配合實地動手寫過才容易完全理解。

【延伸閱讀】

  1. Lambda演算式
  2. 如何透過Lambda精簡Threading程式
  3. Action<T> and Func<T>
  4. UI Thread限制
  5. Closure in C#
MEMO-用Javascript RegExp將<x>置換成<span class="x">

每次都記不太住Javascript RegExp要怎麼做複雜的Replace(例如: 將比對相符的字串內容變成新置換文字的一部分,標題說的"將<x>換成<span class='x'>"就是典型案例),特別記錄一下供未來年老回憶之用。

我遇到的實際需求是想將Sharepoint查詢結果中的高亮註記<c1>, <c2>分別轉成<span class='hl1">, <span class="hl2">。

例如:

、<c2>晶圓</c2>雙雄... 有利<c1>台灣</c1>出口業加速復甦,而<c1>台灣</c1>程式魔人族群亦在持續擴大中,符合投顧的預期...

要轉成

、<span class='hl2'>晶圓</span>雙雄... 有利<span class='hl1'>台灣</span>出口業加速復甦,而<span class='hl1'>台灣</span>程式魔人族群亦在持續擴大中,符合投顧的預期...

程式說穿了不值一文錢,但也好歹讓我試了十來分鐘,不寫下來恐怕下回又得要再花十分鐘,所以有了這篇備忘文。

var s = "、\u003cc2\u003e晶圓\u003c/c2\u003e雙雄... 有利\u003cc1\u003e台灣\u003c/c1\u003e出口業加速復甦,而\u003cc1\u003e台灣\u003c/c1\u003e程式魔人族群亦在持續擴大中,符合投顧的預期...";
var regex = /[<]c(\d)>/g;
s = s.replace(regex, "<span class='hl$1'>");
regex = /[<]\/c\d>/g;
s = s.replace(regex, "</span>");

【後記】這是我目前唯一一個在一顆蕃茄(25min)內搞定的工作項目--研究如何用Javascript置換SPS搜尋結果的高亮CSS,然後拿剩下的10分鐘跟偷偷佔用的休息時間寫了這篇文章 XD

在.NET 3.5中使用Parallel.For()

網友KENCHAO問到"好威的Parallel.For可以用在.NET 3.5上"嗎?

微軟在Task Parallel Library CTP版本時代,曾提供過相容於.NET 3.5的Microsoft Parallel Extensions for .NET Framework 3.5。但找了一下,官方似乎已不再提供該版本的下載... 但是別氣餒,依據MS Parallel Programming RD小組在2009年11月的PO文,有一個來自個DevLabs的替代解決方案--Reactive Extensions to .NET (Rx),其中有個System.Threading.dll可向前相容Parallel Extensions for .NET Framework 3.5(但不提供技術支援),靠著它,我們就可讓Parallel.For在.NET 3.5中也大顯神威~~~

Rx首頁下載【Rx for .NET Framework 3.5 SP1】,安裝後你可以在C:\Program Files\Microsoft Reactive Extensions\Redist\DesktopV2 找到System.Threading.dll。

用VS2008開啟一個.NET 3.5專案,新增參考指向剛才提到的System.Threading.dll,在程式開端加個using System.Threading.Tasks宣告,接著在程式碼中就可以大大方方地使用Parallel.For囉!

不過,要注意幾點:

  1. 在.NET 3.5使用TPL所能展現的效能會遜於.NET 4.0,理由是.NET 4.0在ThreadPool及一些效能調校上還做了額外增強。
  2. 不提供官方支援。
  3. 透過.NET 3.5使用TPL時,無法享用VS2010的Parallel Task/Parallel Stack/Concurrency Visualizer等平行運算專屬新工具。
  4. OperationCanceledException的使用與.NET 4.0的版本有些差異。

雖然有以上限制,但能在.NET 3.5裡用Parallel.For()簡化原本囉嗦的多執行緒寫法,畢竟還是件爽快的事,值得一試!

解決Virtual PC網路分享存取暴慢的問題

在Windows 7使用Virtual PC VM時發現一個嚴重問題,當透過網路分享方式存取VM分享資料夾時,傳輸速度慢到嚇人!

例如: 我的Windows 7上有台Virtual PC VM(IP = 192.168.1.15),當從Windows 7的C:\Softeware Copy檔案到\ \192.168.1.15\Downloads 時,速度暴慢,始終維持在16KB/s以下...

Windows 7與Virtual PC分明共用一張100M Ehternet網卡,卻只能用到128k,會不會太扯了一點? 簡直比扯鈴還扯呀~~~

查詢到MS KB-Slow performance when you try to access resources on your Virtual Server 2005 host computer from a guest virtual machine,裡面提到VM所模擬的DEC Intel 21140A網卡晶片不支援TCP Segmentation Offloading(這個火星術語翻譯成地球語就是: 把拆封包的工作交給網卡,減少CPU的負擔),當主機啟用此功能時會造成網路速度變慢並衍生斷線等問題。

KB建議的解決方式有三種:

  1. 另外新增一張虛擬網卡(Microsoft Loopback Adapter),VM改用虛擬網卡Routing上網。(我覺得這有點為了喝牛奶養牛)
  2. 修改Registry,停用TCP Task Offloading。(此舉可能會增加CPU負擔,但我的CPU使用率很少超過5%,應該算是"九千牛一毛"吧! )
  3. 不想全機停用TCP Task Offloading的話,可以只停用特定網卡的Offloading。(我只有一張網卡,所以用2也沒差)

最後我決定採用方法2,增加HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\DisableTaskOffload後重新開機,再試一次搬檔,40MB/s!!! 對嘛,這才像話!

Sharepoint Web Service NullReferenceException Error

最近在整合Sharepoint的Search.asmx做客製化,一路上波折不斷。

很幸運地,找到一個好用的Open Source查詢工具--Sharepoint Search Sevice Tool,可以提供Scope、欄位資訊,用勾選就可以自動組出Query XML,按鈕後馬上看結果。複雜的Sharepoint Search瞬間被簡化,讓我這個新手在最短的時間可以進入狀況,真是勝造七級浮屠的聖品! (面對全然陌生的技術議題,當你手上有一個可以RUN的Sample,那種安全感無可言喻啊!)

不過,我並沒有因為獲得神奇道具就從此過著幸福快樂的日子。

我要處理的MOSS是由多台Server組成的Web Farm,測試發現同樣要連Search.asmx,某幾台Server OK,有幾台卻怎麼敲帳號密碼都不會過。仔細比對發現,有記憶連線帳號密碼的主機不必問帳號密碼就可以連上;遇上未記憶密碼的Server,怎麼輸入都會得到HTTP 401 UnauthorizedAccess Exception。

追了一下,發現問題出在原程式中未將Domain Name傳成NetworkCredential的第三個參數,修改後就可解決。(參考: NOTES-NetworkCredential Constructor for Domain Account)

殺掉小魔王後,遇到另一個更機車的問題。例如: 我的Web Farm FQDN是sps2007.darkthread.net,其中有五台機器,192.168.100.1 - 192.168.100.5。結果連到192.168.100.1/192.168.100.2 Search.asmx可以正常查詢,連至3, 4, 5三台則在呼叫Search.asmx GetPortalSearchInfo()時出現NullReferenceException。

爬文半天也毫無頭緒,後來心一橫決定鋸箭! 由於呼叫GetPortalSearchInfo是用來取得Scope清單,而另一個Method GetSearchMetadata也可以取得Scope清單,在文件上更是建議用它取代GetPortalSerachInfo。我把GetPortalSearchInfo的程式碼刪除,將Scope擷取邏輯交給GetSearchMetadata,暫時就閃開了這個問題。

只可惜高興不到兩分鐘,這番鋸箭雖然可以讓我連上3,4,5號機列出Scope,但只要一Query,就會彈出System.ArgumentNullException!! 能正確執行的還是只有1,2號機。

到此,我已耗去兩天時間(其間還試著用Reflector查看GetPortalSearchInfo想找出端倪、也設法試跑過Managed Object Model、當然也爬了無數的文),最後,看到有人提及用FQDN失敗、用Machine Name成功的案例,給了我一線生機。

偵辦方向由程式API規格轉到與SPS設定上,於是便問了一下熟悉架構的同事,原來Sharepoint 2007是有玄機的,有個備用存取對應決定了內部URL與公用URL的對應,看到以下設定畫面的那一刻...

一切盡在不言中~~~

CODE-Save ADO.NET DataTable As CSV

之前寫過將CSV檔案內容轉換成ADO.NET DataTable物件,今天的需求剛好反過來,要將DataTable的內容匯出成CSV,邏輯上簡單許多,但還是PO文一篇,下回需要時比較好找。

static string Quoted(string v) {
    return "\"" + v.Replace(@"""", @"""""") + "\"";
}
 
static void SaveDataTableAsCSV(DataTable t, string csvPath)
{
    StringBuilder sb = new StringBuilder();
    List<string> l = new List<string>();
    foreach (DataColumn c in t.Columns)
        l.Add(Quoted(c.ColumnName));
    sb.AppendLine(string.Join(",", l.ToArray()));
    foreach (DataRow r in t.Rows)
    {
        l.Clear();
        for (int i = 0; i < t.Columns.Count; i++)
            l.Add(Quoted(r[i].ToString()));
        sb.AppendLine(string.Join(",", l.ToArray()));
    }
    File.WriteAllText(csvPath, sb.ToString(), Encoding.UTF8);
}

註:

  1. File.WriteAllText時要加註Encoding.UTF8以確定輸出檔案會包含BOM檔頭,Excel才能正確開啟。
  2. 測試發現,欄位內容值即使包含換行符號,也能被Excel正確解析,讓我驚喜了一下。
NOTES-產生具有BOM的UTF8編碼檔案

上回有討論過Excel開啟CSV時的中文編碼問題,今天發現關於.NET處理BOM的幾個特性,再補充三則筆記:

  • 雖然預設UTF8Encoding的encoderShouldEmitUTF8Identifier參數預設為true,但GetBytes()的結果不會包含BOM
  • File.WriteAllText與StreamWriter在沒有指定Encoding.UTF8時,會產出UTF-8編碼但沒有BOM的檔案
  • 以下的範例中,只有F2.csv、F4.csv可以正確被Excel開啟,原因請見上回文章
string s = "牛,牪,犇";
File.WriteAllText("B:\\F1.csv", s);
File.WriteAllText("B:\\F2.csv", s, Encoding.UTF8);
using (StreamWriter sw = 
    new StreamWriter("B:\\F3.csv", false))
{
    sw.WriteLine(s);
    sw.Close();
}
using (StreamWriter sw =
    new StreamWriter("B:\\F4.csv", false, 
        Encoding.UTF8))
{
    sw.WriteLine(s);
    sw.Close();
}
NOTES-NetworkCredential Constructor for Domain Account

這問題之前曾遇過幾次,但處理得有些含糊,這回特別做了測試釐清。

【疑問】要用NetworkCredential設定存取身份時,網域帳號可否寫成"domainName\userName",例如: new NetworkCredential("domainName\\userName", "password") ?

不知為什麼,我一直記得這樣寫是可行的(也許因為Windows的登入視窗,可以選擇domainName\userName或將domainName寫到第三個欄位吧?),今天實地做了測試,答案是: (分別測試了Windows 2000/Windows 2003、在Web本機存取自己的Web、本網域/信任網域...)

不行!!! 請乖乖寫成new NetworkCredential("userName", "password", "domainName")

不過,為求方便,我們應該要允許使用者輸入"domainName\userName",然後在程式裡用以下方法快速拆解:
if (userName.Contains("\\")) {
    string[] p = userName.Split('\\');
    cred = new NetworkCredential(p[1], password, p[0]);
}

【網友經驗】

Posted 28 January 2010 04:37 PMJeffrey | no comments
Filed under:
【茶包射手日記】失落的change事件

有個網頁在某欄位的change事件掛了一段邏輯,依輸入內容連動其他欄位值。使用者抱怨上個月第一次使用完全正常,這個月再用時,在該欄位中輸入資料,其他欄位卻未跟著連動...

我用自己的Client連到同一網頁,跟User輸入同樣的值,一切正常。移駕到使用者座位,使用"肇事"機器實地操作,連動功能也完全正常! 莫非,這程式會認主人,只要遇到拎杯親自操作就不敢造次?

世界上有很多無法解釋的玄妙事件,但本案例並不包含在內。依茶包射手實戰手冊第748頁的記載,此種靈異現象通常是User與Developer在操作上有細微差異才造成不同結果,最好的處理方式是請User依"平日習慣"操作一次,實地進行觀察。終於發現端倪了! 原來,使用者這個月Key單時輸入該欄位的資料跟上月雷同,於是好心的IE亮出了"自動完成"提示(如下圖示意),能少打字當然要省,User使Click一下自動帶入;而我在測試時,為了確保change被觸發,潛意識驅使之下是一個字母一個字母手動輸入。重點來了--IE的自動完成有一項特性,它 不 會 觸 發 change 事 件 !

之前在測試開發階段,因偷懶加掛了One Click自動填表功能對手動測不多;而更早的手動測試階段,雖然曾反覆輸入不同值做測試,因表單沒有Submit,並不會納入自動完成提示清單(When a user submits a form, the name, value, and domain of the form component are encrypted for safekeeping),因此還是錯失"以自動完成輸入"的機會。

事件成因清楚了,要解決就不難。我想到幾種做法:

  1. 利用<input autocomplete="off" />關閉自動完成功能。
  2. 改用blur取代change,但缺點是輸入值未變時也會觸發不必要的連動邏輯。
  3. 用onpropertychange取代onchange,但缺點是會有跨瀏覽器問題,再不然就是針對IE加Javascript針對不同瀏覽器採行不同的做法。(鄉親吶~~ 看清楚,這就是跨瀏覽器要付出的代價呀!)
生活瑣記-201001
  • 自從有了DSLR胡亂玩了好些年,也不乏曾為追逐天晴大景背著沈重相機搶登山巔,
    總以為要有好照片少不了景好技術佳加上老天賞臉,
    直到前幾天,用老爺CASIO相機拍下這張半糊的照片,
    我才驀然發現,
    其實,照片是否引起悸動挑動心絃,無關乎構圖快門或光圈...


  • 幫兒子洗澡時,他好奇地摸著自己的"蛋蛋"誠心發問: 這個是做什麼用的呀?
    鄉親吶~~~ 在黑暗浴室臨時舉辦的【百萬小學堂】,關於這道【 大班 自然與生活科技】問題,你會怎麼回答呢?
    (計分標準: 需以五歲小朋友可以理解的語言傳達正確知識,搞笑胡扯搪塞迴避者不予計分。另因臨時舉辦,故小西瓜、大力都沒來,所以也沒有好人卡、團結卡、知識卡可以用囉...)
    至於我的答案? 先賣個關子,寫在文末。
  • 車子的大燈壞了一顆,看在車主手冊有寫拆裝步驟的分上,決定自己賺工錢。
    在網站比了價(幸好有先做功課,查到網購價最低140,老闆原本想唬我燈泡價位不可能這麼低),在汽車材料行花150元買了一顆歐司朗H1,花了點時間模索... 哇哈哈! 黑暗水電工邁出挑戰【黑暗修車工】的第一步囉!

  • 陽台上有一棵種了兩年的香水檸檬,原本是跟花販買的小盆裁,入手時就帶了一棵拳頭大小的果實。
    據說它是極好種的植物,隨便養都能果實纍纍。無奈放在陽台日照不足,加上平時也沒在園藝下功夫,在咱們家隨便種的結果,就是它也敷衍長。兩年來下再多肥料也只長葉子,開花次數用一隻手都可以數出來,更甭提結果實。所幸半年前有兩朵花順利修得正果,結出香水檸檬來。
    這讓人頗有老來得子的振奮感,當然就捨不得摘,日益變大之際還怕樹枝不堪負荷,在下面墊了個玻璃瓶權充嬰兒椅。這陣子果實開始變黃,看來己熟透,摘下前特地拍照一張留念。
    圖中有個一元硬幣可當比例尺,讓大家感受一下它的體積。其實也還好啦,還是像拳頭一般大,只是這回是像"砂鍋大的拳頭"一樣大,很強吧? XD

百萬小學堂的【 大班 自然與生活科技】問題大家有解答了嗎? 在此公佈黑暗版的答案:

"這是用來裝生小孩【種子】的地方,你要保護好哦!"

兒子想起在開心農場種馬鈴薯播種發芽的過程,很滿意地點點頭,還自行演繹出"哦! 所以把種子放到女生的肚子裡,就可生小Baby囉!"

怎樣,這個比喻超讚吧?

CODE-列舉元素已掛載的jQuery事件

開發程式時偵錯的需求,想確認預期的事件函數是否已正確bind到元素上?

直覺想法是去查詢jQuery內部物件,列出已經掛載的事件函數。追了一下原始程式,發現jQuery會把各元素的事件保存在jQuery.data(elem, "events"),而events裡又會為不同事件(例如: click, dblclick, load)各宣告一個handlers,放入events[eventType];由於我們可以對同一事件宣告多個事件函數,因此handlers中會以handlers[handlerId]的方式保存事件函數。

看起來很抽象,跑一次範例便一目瞭然:

$("div")
.click(function() { alert("Click 1"); })
.click(function() { alert("Click 2"); })
.dblclick(function() { alert("DblClick"); });
var handlers = jQuery.data($("div")[0], "events");
var sb = [];
for (var t in handlers)
{
    sb.push("*EventType=" + t);
    for (var h in handlers[t])
        sb.push("    HandlerId[" + h + "]->" + handlers[t][h]);
}
alert(sb.join("\n"));

可得結果如下: (程式裡所謂HandlerId是一個流水號,是jQuery.event物件為每個事件函數所賦與不重複的編號)

*EventType=click
    HandlerId[3]->function() { alert("Click 1"); }
    HandlerId[4]->function() { alert("Click 2"); }
*EventType=dblclick
    HandlerId[5]->function() { alert("DblClick"); }

實地測試,這個做法在jQuery-1.3.2或jQuery-1.4下都是可行的。但在jQuery 1.4下HandlerId會從1起跳,背後原因是jQuery-1.4取消了原本在bindReady()裡jQuery.event.add( window, "load", jQuery.ready );,以及防止IE Memory Leak的邏輯中改用attachEvent取代jQuery( window ).bind( 'unload', ...);,少了兩個系統內建事件函數,自訂事件函數序號變成從1開始。

【註】以上的做法列舉範圍只限透過jQuery.bind方式宣告的事件函數。

用.NET展現多核威力(3) – 佛心TPL之Parallel.For好威

前一篇文章裡,我們驗證了為每個CPU Core開一條獨立Thread並事先分攤好計算工作,可以讓巨量Log10計算程式飆出最高效能! 但是,仔細看看程式碼:

int WORKER_COUNT = 2;
Thread[] workers = new Thread[WORKER_COUNT];
int jobsCountPerWorker = MAX_COUNT / WORKER_COUNT;
for (int i = 0; i < WORKER_COUNT; i++)
{
    int st = jobsCountPerWorker * i;
    int ed = jobsCountPerWorker * (i + 1);
    if (ed > MAX_COUNT) ed = MAX_COUNT;
    workers[i] = new Thread(() =>
    {
        for (int j = st; j < ed; j++)
        {
            double d = Math.Log10(Convert.ToDouble(j));
        }
    });
    workers[i].Start();
}
for (int i = 0; i < WORKER_COUNT; i++)
    workers[i].Join();

我們寫了近20行的程式碼,而且還得花腦筋寫邏輯分割工作給多條Thread,要曉得如何用Thread.Join同步完成時間。說實在話,沒有三兩三,恐怕沒膽玩。

如果我說有一種很簡單的新寫法可以實現類似的效果:

Parallel.For(0, MAX_COUNT, j =>
{
    double d = Math.Log10(Convert.ToDouble(j));
});

看到這裡,大家會不會有想起立鼔掌的衝動?

這是.NET Framework 4.0裡內建的Task Parallel Library(TPL),一組幫助程式新手的佛心API。把原本複雜的多執行緒運算程式簡化成一行打死,寫程式的人就算對Thread.Join、lock()、ManualResetEvent一無所悉,照樣可以寫出漂亮的平行運算程式。(這下子,程式老鳥又有一項優勢被剝奪了,被菜鳥幹掉的日子愈來愈近了,我好怕...)

依我個人的理解,TPL強調的是平行運算的能力,目的在搾乾每一滴CPU運算能力。它在概念上介於ThreadPool與自行管理數條Thread之間,最精彩的地方是會依CPU的負載狀況自動調節Thread數,直到所有CPU使用率都飆上100%,系統運算能力完全被榨乾為止。換句話說,一開始迴圈只啟動一條Thread,接著會在資源允許的前題下增加平行處理的Thread數(the loop starts with a degree of 1, and may work its way up to any maximum that’s specified as resources become available,參考)。

接著,我們就讓原來寫法跟Parallel.For車拼一下(測試平台為E6400雙核CPU): (註: 要體驗.NET 4.0,請先下載VS2010 Beta回家玩)

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
 
namespace MultiCore
{
    class TestParalleFor
    {
        static void Main(string[] args)
        {
            int MAX_COUNT = 5000 * 10000;
            Stopwatch sw = new Stopwatch();
            for (int round = 0; round < 3; round++)
            {
                sw.Reset();
                sw.Start();
                int WORKER_COUNT = 2;
                Thread[] workers = new Thread[WORKER_COUNT];
                int jobsCountPerWorker = MAX_COUNT / WORKER_COUNT;
                for (int i = 0; i < WORKER_COUNT; i++)
                {
                    int st = jobsCountPerWorker * i;
                    int ed = jobsCountPerWorker * (i + 1);
                    if (ed > MAX_COUNT) ed = MAX_COUNT;
                    workers[i] = new Thread(() =>
                    {
                        for (int j = st; j < ed; j++)
                        {
                            double d = Math.Log10(Convert.ToDouble(j));
                        }
                    });
                    workers[i].Start();
                }
                for (int i = 0; i < WORKER_COUNT; i++)
                    workers[i].Join();
                sw.Stop();
                Console.WriteLine("Multi-Thread[{1}] = {0:N0}ms", 
                    sw.ElapsedMilliseconds, WORKER_COUNT);
 
                sw.Reset();
                sw.Start();
                Parallel.For(0, MAX_COUNT, j =>
                {
                    double d = Math.Log10(Convert.ToDouble(j));
                });
                sw.Stop();
                Console.WriteLine("Parallel.For = {0:N0}ms",
                    sw.ElapsedMilliseconds);
            }
            Console.Read();
        }
    }
}

Multi-Thread[2] = 2,003ms
Parallel.For = 2,119ms
Multi-Thread[2] = 2,098ms
Parallel.For = 2,283ms
Multi-Thread[2] = 1,966ms
Parallel.For = 2,113ms

就數據而言,二者相近,但在這個例子中,大部分的時間Parallel.For還是略輸一籌。理由是Parallel.For強調的是動態調節,由一條Thread開始,再逐步增加,自然會比事先規劃好全程用兩條Thread衝刺慢一些。不過別沮喪,我們動個手腳,馬上就能還Parallel.For一個公道。

我們將:

double d = Math.Log10(Convert.ToDouble(j));

改成每5000次Delay 10ms: (假裝在等待某項非CPU資源)

double d = Math.Log10(Convert.ToDouble(j));
if (j % 5000 == 0) Thread.Sleep(10);

並把執行次數改為100萬次縮短總執行時間。

Multi-Thread[2] = 1,046ms
Parallel.For = 652ms
Multi-Thread[2] = 1,009ms
Parallel.For = 502ms
Multi-Thread[2] = 1,007ms
Parallel.For = 550ms

怎樣? 前一個測試二者結果相近,加入Thread.Sleep後比原來的一核一緒寫法快了近一倍,程式簡潔N倍,在這兩回合比試中,我認定Parallel.For明顯勝出!!

因為Thread.Sleep的加入會降低CPU使用率,Parallel.For動態增加Thead的能力便可派上用場,填補了CPU空檔,也就縮短了總執行時間。在實務上,即便是以運算為主的工作,還是免不了有等待I/O、等待其他Thread就緒的同步需求,必須暫停等待其他非CPU資源後再繼續,等待期間會產生CPU使用率下降的情況。平行運算哲學中,讓CPU閒著是一種罪惡,在CPU使用率未達100%時增加Thread數充分利用閒置的運算能力,自然會比一核一緒更上一層樓。要自己寫出視CPU使用率動態調節Thread數的程式並非易事,而Parallel.For可以幫我們做到這一點,很威吧? 大家如果在.NET 4.0中開發類似的平行運算需求時,千萬不要錯過它囉~~

[2010-02-05補充] 若想在.NET 3.5中使用 Parallel.For(),可以參考這篇文章

NOTES-客製化Sharepoint查詢研究筆記

最近工作上遇到需要客製Sharepoint查詢的需求,這篇主要是研究過程的隨手筆記,避免明天一覺醒來忘到一乾二淨。由於內容較雜亂無章,請讀者自行斟酌是否要直接略過。

  • 文件: MOSS 2007 Custom Search
    1. 先考慮用現成Search,可以節省大把時間。SPS2007已經比SPS2003先進方便很多--Business Data Content(BDC)。
    2. 真需客製時可用Microsoft.Office.Server.Search.Query建立WebPart
    3. 跨系統搜索(例如: SPS, CMS)的做法
      1) 建立BDC Application Definition, 匯入SSP(Shared Services Provider)。其中要實作ProductIDEnumerator method,用來傳回Primary Key,讓Sharepoint可以用它讀資料回來做Index。
      2) 匯入時會建立BDC Entities的網頁,可用來檢視資料是否正確匯入。
      3) 在SSP中加入DBC Source,開始Crawl
      4) 建立自訂屬性對應到爬回來的屬性,加入一些進偕搜尋可以用來篩選的條件(Mapping時可設定順位,若沒A,用B)
      5) 重新再爬一次,這次會對應回Managed Properties。
      6) 自訂搜尋吧! 有三種方法:
          * Keyword Query syntax (直接送給Enterprise Search service)
          * SQL syntax (加料式SQL語法)
          * URL syntax (直接串連搜尋介面網頁)
    4. SQL法範例:
      SELECT        Title, Path, Description, HitHighlightedSummary, Rank, ProductMembershipRank, DomesticRegion, ProductCategory
      FROM            SCOPE()
      WHERE         FREETEXT(Description,'Fun') AND ProductCategory = 'Accommodation'
      AND DomesticRegion = 'Australia''s South West'
      ORDER BY MembershipRank DESC, RANK DESC
    5. SQL法的傳回結果:
      FullTextSqlQuery ftsq = new FullTextSqlQuery(ServerContext.Current); ftsq.ResultTypes = ResultType.RelevantResults;
      ftsq.QueryText = <YOURQUERY>;
      //Return the search results to a ResultTableCollection,可視為DataSet用
      ResultTableCollection results = ftsq.Execute()
    6. ResultTableCollection,可視為DataSet用,同時有ElapsedTime, HitHighlightedSummary等好用屬性。
  • 官方文件: Enterprise Search Architecture
    • 組成元件: Index Engine, Query Engine, Protocol Handlers(將不同管道取得的原始資料中的文件取出來), IFilters(取出文件中的文字內容與屬性), Content Index, Property Store, Search Configuration Data, Wordbreakers
    • Content Crawling: Filter Daemon使用Protocol Handler把資料取回來,用IFilter去取出其中的文字與Metadata,做全文檢索Index時會用到Workbreaker,Property與Index是分開存放的。Property存放處還一併記錄了文件層級的安控資訊。
    • Search Query Engine: 搜尋關鍵字會先經Wordbreaker處理。有加Property條件時,會先用Index查,再依查到結果去取回屬性資料做第二層篩選,沒權限的查詢結果項目也會被刪除。
    • 支援的資料來源: Sharepoint Content, Web Content, File Share, Exchange Folder Content, Business Data Content
    • Shared Scopes: 用一些條件把內容項目定義成一個特定範圍,可用的Rule包含: Address, Property Query, Content Source
    • Document Property Mappings: 有兩種Property-Crawled Prop.及Managed Prop.,二者間可以設定Mapping。
    • Server Mapping: 可以設定查到的結果怎麼Mapping成特定的URL,例如: File Share -> Web Link
    • Revevance Inclusions: 計算Ranking,改變結果項目顯示順序,可能的參數包含: Click distance, Hyperlink anchor text, URL surf depth, URL text matching, Automated metadata extraction, Automatic language detection, File type relevancy biasing, Enhanced text analysis
    • File Type Inclusions: 決定Crawler哪些檔案類別要擷取哪些不要
    • Logging: Query Log, Crawl Log
    • Site Level的搜尋管理: Scope, Keyword and best bet
  • 官方文件: Building Enterprise Search Queries
    • Keywords
      • 關鍵字: Keyword1 +Keyword2 -Keyword3
      • 屬性:
        <property name>:<value>
        <scope name>:<value>
        <collapsed results type>:<value>
      • 屬性範例: site:http://blog.darkthread.net, author:"Jeffrey Lee", scope:"ScopeName", duplicate:http://<displayUrl>(這個看不懂)
      • 查詢時,不會檢查URL, SiteName, File Ext,就是查不到東西
    • Enterprise Search SQL Language
      • 以ANSI-SQL為基礎
      • 不一定要精確,例如: program <-> programming
      • rank欄位會傳回吻合度,0-1000
      • 有些(謎之聲: 這叫很多吧?)SQL語法不可用: CONVERT(但CAST可以)、CREATE VIEW、DATASOURCE、AVG(), COUNT(), MAX(), MIN(), SUM(), GRANT, INSERT, CONTAINS, LIKE, MATCHES, 關聯欄位相比較(不會吧?), Parameter, REVOKE, SCOPE, SELECT ALL, Stored Procedure, UNKNOWN, UPDATE, BATCH...
      • 要指定Scope => SELECT ... FROM Scope() WHERE "scope" = "All Sites"...
      • CINTAINS精確比對,FREETEXT類似比對
      • LIKE, string/date/timestamp/numeric可以用大小於比對, NULL代表未定義
      • WHERE LastModifiedTime <=DATEADD (DAY, -2, DATEADD (HOUR, -4, GETGMTDATE()))
      • ARRAY [1,2] < ARRAY [1,2,3]
    • URL法
      • 參數: k-關鍵字, s-Scope, v=date or relevance, start=頁數
    • Enterprise Search Query Object Model
      • Web Part or ASPX
      • Microsoft.Office.Server.dll, Microsoft.Office.Server.Search.dll, Microsoft.SharePoint.dll
      • 物件
      • 用哪一個物件? 複雜時KeywordQuery會無法勝任,就用FullTextSqlQuery
    • Web Service: httq://Server_Name/[sites/][Site_Name/]_vti_bin/search.asmx
      • Query只傳回Title, Desc, Date, Relevance,QueryEx還可傳回Rank, Author, Size, Path...等。
      • QueryEx傳回DataSet!! (Yahoo~~~)
    • 自訂結果存取權限管控,決定User該不該看到該筆查詢: ISecurityTrimmer
    • 客製查詢結果: 修改XSLT
    • 客製查詢中心
    • 寫程式管理Search Content、Metadata、Scope、Keyword...
    • Featured Search: 在查詢結果中混入其他來源的結果(置入性行銷?)
  • 佛心來著的Sharepoint Search Service Tool: 可以輔助產生Query Syntax、馬上看結果的好東西
用.NET展現多核威力(2A) - 一核一緒補充包

前一篇多核研討文章中,用了一個計算1000萬次Log10運算的範例驗證Thread數與Core相同時可以達到最佳效能,網友Google質疑以Log10計算當範例是否用能代表"以運算為主的大量作業",在此做點補充說明。

我想若以茶包射手實事求是的精神,"以運算為主的大量作業"這個命題是有問題的,應該要修正成"不涉及非CPU資源競爭的大量純運算作業"更貼近原意。用白話來解釋,這裡假設的前題是---有一大堆運算工作要處理,每件運算工作彼此獨立可以同時進行,且每件運算所需的資料自給自足,不需要排隊讀取/寫入記憶體、磁碟、網路等資源。當此前題成立,理論上連續大量運算就會耗光單一CPU的所有運算資源,讓CPU呈現100%佔用狀態。因此不管是"簡單的Log10計算"或是"複雜的超大矩陣運算"應該都能得到相同的結果--要達到最高效能,就是讓每顆CPU都忙到最高點,並避免Thread過多時要耗費CPU去做Context Switch拖累效能。以這個概念演繹,為每一核安排一條執行緒應為最佳解。

補充完前回理論命題的不足,我們也來試一下"矩陣運算的範例",看看是否可以獲得一核一緒的結論?

不想自己造輪子,這裡用現成"內含小型矩陣運算的DES加密"當作範例,但複雜度己比Log10高出許多。我用亂數產生了1000個長度為512KB的byte[],分別用1-16條的Thread分工合作處理對這1000筆資料做DES加密,不同Thread數的測試各做五次,取其總耗用時間的平均值。

using System;
using System.Diagnostics;
using System.Threading;
using System.Security.Cryptography;
using System.Text;
using System.IO;
using System.Collections.Generic;
 
namespace MultiCore
{
    class ComplexCalc
    {
        static void Main(string[] args)
        {
            int MAX_COUNT = 1000;
            Dictionary<int, byte[]> pool = new Dictionary<int, byte[]>();
            Random rnd = new Random();
            for (int i = 0; i < MAX_COUNT; i++)
            {
                byte[] b = new byte[512 * 1024];
                rnd.NextBytes(b);
                pool.Add(i, b);
            }
            int ROUND_COUNT = 5;
            Stopwatch sw = new Stopwatch();
            for (int WORKER_COUNT = 1; WORKER_COUNT < 16; WORKER_COUNT++)
            {
                long sum = 0;
                List<string> detail = new List<string>();
                for (int round = 0; round < ROUND_COUNT; round++)
                {
                    sw.Reset();
                    sw.Start();
                    Thread[] workers = new Thread[WORKER_COUNT];
                    int jobsCountPerWorker = MAX_COUNT / WORKER_COUNT;
                    for (int i = 0; i < WORKER_COUNT; i++)
                    {
                        int st = jobsCountPerWorker * i;
                        int ed = jobsCountPerWorker * (i + 1);
                        if (ed > MAX_COUNT) ed = MAX_COUNT;
                        workers[i] = new Thread(() =>
                        {
                            DESCryptoServiceProvider des =
                                new DESCryptoServiceProvider();
                            des.Key = Encoding.UTF8.GetBytes("ABCDEFGH");
                            des.Key = Encoding.UTF8.GetBytes("12345678");
                            for (int j = st; j < ed; j++)
                            {
                                byte[] randomContent = pool[j];
                                using (MemoryStream ms = new MemoryStream())
                                {
                                    using (CryptoStream cs = new CryptoStream(ms,
                                        des.CreateEncryptor(), CryptoStreamMode.Write))
                                    {
                                        cs.Write(randomContent, 0, randomContent.Length);
                                        cs.FlushFinalBlock();
                                    }
                                }
                            }
                        });                          
                        workers[i].Start();
                    }
                    for (int i = 0; i < WORKER_COUNT; i++)
                        workers[i].Join();
                    sw.Stop();
                    sum += sw.ElapsedMilliseconds;
                    detail.Add(sw.ElapsedMilliseconds.ToString());
                }
                Console.WriteLine("平行處理[{1}] = {0:N0}ms [{2}]",
                    sum / ROUND_COUNT, WORKER_COUNT, string.Join(",", detail.ToArray()));
            }
            Console.Read();
        }
    }
}

有幸借到一台8核CPU的神器,在上面測試得到的結果,應該可讓我們對"一核一緒"的推論更有信心。

平行處理[1] = 14,838ms [14825,14804,14834,14913,14815]
平行處理[2] = 7,942ms [7932,7781,8069,7992,7938]
平行處理[3] = 5,224ms [5434,5234,5138,5160,5158]
平行處理[4] = 3,976ms [3952,3941,3975,3959,4053]
平行處理[5] = 3,312ms [3299,3322,3374,3305,3261]
平行處理[6] = 2,900ms [2874,2905,2913,2915,2897]
平行處理[7] = 2,615ms [2566,2646,2605,2670,2592]
平行處理[8] = 2,540ms [2526,2449,2506,2566,2654]
平行處理[9] = 2,641ms [2809,2588,2713,2522,2576]
平行處理[10] = 2,726ms [2737,2706,2710,2809,2672]
平行處理[11] = 2,684ms [2724,2638,2670,2625,2767]
平行處理[12] = 2,771ms [2796,2795,2744,2717,2805]
平行處理[13] = 2,755ms [2648,2764,2665,2710,2990]
平行處理[14] = 2,726ms [2741,2705,2809,2711,2667]
平行處理[15] = 2,781ms [2862,2686,2792,2837,2732]

陸續用過不同的演算內容(共同等徵是自給自足的純CPU運算不涉及其他資料共用)做相同測試,大致的結果都很相近--Thread數增加時,耗用時間會遞減,最短的時間會出現在Thread數與CPU核數相同時;而Thread超過CPU核數時,時間會略為增加,但並不會隨Thread數繼續上升而明顯增加,而是呈現隨機變化(以上為例: 11變快,12變慢,13又比12快,反覆做了幾次,發現其無一定規則)。針對這個觀察,我的解釋是,Thread數比CPU核數略多時,產生的Context Switch狀況有限,故對效能的影響也不大。經過多次實驗,耗用時間最短的數據出現在Thread數等於CPU核數這點倒是被獲得證實。

平行運算是門深奧的學問,要深入探討學術理論遠超出一般程式開發人員的能力範圍。不過,依茶包射手的精神,在能力許可範圍親手實驗一下與自己切身相關的效能議題,體驗意想不到的樂趣有何不可? 上述的範例算是一個有趣的實驗基礎,稍加修改就可以用來驗證不同運算需求在不同核緒比例下的效能表現,有興趣的人可以玩玩,檢視一下不同運算需求的最佳核緒比例,也歡迎有不同心得的朋友分享切磋。

更多文章 下一頁 »

搜尋

Go

<February 2010>
SunMonTueWedThuFriSat
31123456
78910111213
14151617181920
21222324252627
28123456
78910111213
 
RSS
【工商服務】
最新回應

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


BlogLook Score and Rank

Syndication