1. Canon G9 害我沒睡好... 相片自動轉正的問題

    抱怨一下,因為在看照片時發現,有些直的拍的照片看起來是正確的 (會自己轉 90 度),有些卻不是... 得歪著頭看,所以就很好奇到底是怎麼回事...。

     

    image

    這幾張是正確的 (右上 & 左下,會自動轉 90 度)

     

    image

    這幾張是錯的 (應該自動轉 90 度才對)

     

    image

    第二張是錯的 (應該要轉 180 度才對)

     

    越想越不對,我記得除了我那台古董 CANON G2 之外,後來 CANON 的相機都有加上偵測轉向的機制啊? 不外乎是水銀開關或是類似的東西,總之相機能夠得知目前是不是轉直的拍,同時能把這資訊寫到 EXIF 內的 ORIENTATION 欄位去...

    但是為什麼拍出來的相片有的可以自動轉,有的不行? 花了點時間歸納一下,發現 G9 拍出來的 "有機會" 是正常的,而家裡大人的 IXUS55 則都不會自動轉,真是怪了... 在相機上看都會自動轉正啊...

    因為我的照片都是自己用 WPF 寫程式縮圖處理的,我開始懷疑是不是我的歸檔程式的問題。G9 拍的 .CR2 檔,透過 RAW CODEC 轉成 JPEG 會自動轉正,G9 / IXUS55 拍的 JPG 檔則不會...

     

     

    image

    嗯,開始無聊了,拿起相機拍了四種角度,然後用 DEBUGGER 去看 EXIF 的 ORIENTATION 值為啥... .CR2 要用 "/ifd/{ushort=274}" 來查,會得到一個 UInt16 的值,如果是 .JPG 則要改成 "/app1/{ushort=0}/{ushort=274}" ...

    得到的值還真怪。.CR2 / .JPG 都一樣。依照上圖的順序,得到的值分別是 0x01, 0x08, 0x01, 0x06。查了查文件,除了轉 180 度那個的值不大對之外,另外三個都正常。不過 Canon Codec 在 decode 時會自動幫我做 RotateTransform,我得在處理 .JPG 時自己補上這個動作。除了轉 180" 之外其它都正常了。

     

    就是那張 180 度的不正確,害我氣的牙癢癢的... 決定跟它拼了... 改了改程式,把所有 EXIF 都印出來,用肉眼一個一個比... 本來想說是不是有別的 FLAG 可以判定出來正反? 結果看到眼睛脫窗了也沒找到,我連 EXIF 裡的 BLOB (Binary data) 都一個一個印出來看 @_@,GOOGLE 也找不到啥有用的資訊...

    最後氣到,拿起相機重拍一次,這次直接在相機上看,按下 DISPLAY 看看有無其它資訊... COW,終於發現... 相機自己也認不出轉 180 度的狀況? 嘖嘖... 搞了半天 CANON 只有偵測左轉及右轉 90 度的情況,轉 180 度就不理它了.. 哈哈!

    不過有誰會轉 180 度拍照? 當然有...

    1. 用右手拿相機自拍,按不到快門... 只好轉 180 度,用底下的姆指來按
    2. 小孩拿著自己亂拍
    3. 想不出來了...

    總算水落石出,犧牲了幾個小時的睡覺時間,咳咳... 不過既然本版都是討論 .NET 程式設計的,最後就貼點 CODE 充個數... 也算沒偏離主題了 :D 不過我想應該沒人像我一樣無聊在搞這些吧?

     

    取得 ORIENTATION 的值,並且判定要往那個方向旋轉[copy code]
                            BitmapMetadata metadata = null;                        Rotation rotate = Rotation.Rotate0;                        // ...	                        ushort value = (ushort)metadata.GetQuery("/app1/{ushort=0}/{ushort=274}");                        if (value == 6)                        {                            rotate = Rotation.Rotate90;                        }                        else if (value == 8)                        {                            rotate = Rotation.Rotate270;                        }                        else if (value == 3)                        {                            rotate = Rotation.Rotate180;                        }
    
       1:  BitmapMetadata metadata = null;
    
       2:  Rotation rotate = Rotation.Rotate0;
    
       3:  // ...  
    
       4:  ushort value = (ushort)metadata.GetQuery("/app1/{ushort=0}/{ushort=274}");
    
       5:  if (value == 6)
    
       6:  {
    
       7:      rotate = Rotation.Rotate90;
    
       8:  }
    
       9:  else if (value == 8)
    
      10:  {
    
      11:      rotate = Rotation.Rotate270;
    
      12:  }
    
      13:  else if (value == 3)
    
      14:  {
    
      15:      rotate = Rotation.Rotate180;
    
      16:  }
    

     

     

     

    利用 TransformGroup, 一次套用 ScaleTransform (縮放) + RotateTransform (旋轉) 兩種轉換特效[copy code]
                JpegBitmapEncoder target = new JpegBitmapEncoder();            TransformGroup tfs = new TransformGroup();            tfs.Children.Add(new ScaleTransform(0.1, 0.1));            switch (rotate)            {                case Rotation.Rotate90:                    tfs.Children.Add(new RotateTransform(90));                    break;                case Rotation.Rotate180:                    tfs.Children.Add(new RotateTransform(180));                    break;                case Rotation.Rotate270:                    tfs.Children.Add(new RotateTransform(270));                    break;            }            target.Frames.Add(BitmapFrame.Create(                    new TransformedBitmap(sourceFrame, tfs),                    null,                    null,                    null));            target.QualityLevel = quality;            //            // save to temp file            //            string temp = Path.Combine(Path.GetDirectoryName(trgFile), string.Format("{0:N}.tmp", Guid.NewGuid()));            FileStream trgs = File.OpenWrite(temp);            target.Save(trgs);            trgs.Close();
    
       1:  JpegBitmapEncoder target = new JpegBitmapEncoder();
    
       2:  TransformGroup tfs = new TransformGroup();
    
       3:  tfs.Children.Add(new ScaleTransform(0.1, 0.1));
    
       4:  switch (rotate)
    
       5:  {
    
       6:      case Rotation.Rotate90:
    
       7:          tfs.Children.Add(new RotateTransform(90));
    
       8:          break;
    
       9:      case Rotation.Rotate180:
    
      10:          tfs.Children.Add(new RotateTransform(180));
    
      11:          break;
    
      12:      case Rotation.Rotate270:
    
      13:          tfs.Children.Add(new RotateTransform(270));
    
      14:          break;
    
      15:  }
    
      16:  target.Frames.Add(BitmapFrame.Create(
    
      17:          new TransformedBitmap(sourceFrame, tfs),
    
      18:          null,
    
      19:          null,
    
      20:          null));
    
      21:  target.QualityLevel = quality;
    
      22:  //
    
      23:  // save to temp file
    
      24:  //
    
      25:  string temp = Path.Combine(Path.GetDirectoryName(trgFile), string.Format("{0:N}.tmp", Guid.NewGuid()));
    
      26:  FileStream trgs = File.OpenWrite(temp);
    
      27:  target.Save(trgs);
    
      28:  trgs.Close();
    

    2008/08/28 .NET Tips WPF 技術隨筆 有的沒的

  2. 敗家: HTPC

     

    image

    沒錯,我又敗家了... :D

     

    想弄 HTPC 已經很久了,不過老是碰到一些怪問題。原本想直接用桌機,不過桌機已經接了雙螢幕,要再接 LCD TV,沒在主機板上插兩張 VGA 卡就辦不到 (其它用 PCI / USB 之類的效能太差就不考慮了)。不過三個輸出都需要數位輸出 (兩台桌上的 LCD 都有 DVI,SHARP TV 又很有個性的不提供 D-SUB,只有 HDMI ... 不然就是色差...),沒有插兩張 DUAL DVI 的中價位 VGA 卡也沒辦法...。我用不到這麼高級的顯示卡啊 :~~~

    不過就算硬體搞定了也是個大問題。試過暫時接到 TV 過,有時我要 MCE 開出來開在桌上的 LCD,有時又要開在 TV ... 光是操作就很麻煩...。最後體認到了沒有弄一台專用機的話,HTPC大概只能試一下試爽的而以。

    與其是想升級換張可以插兩張顯卡的主機板 + 新的顯卡,實在不是太便宜... 尤其我須要的是 DVI x 2 + HDMI ... 後來無意間看到一堆在介紹 AMD 780G 的文章... 算一算好像比換主機板還便宜...。

    於是不知不覺,八月初某日下班就多提著一袋零件回來了... 哈哈。因為有一堆東西有備料,陸陸續續買了下列東西就開始享受了:

    1. 主機板: ASUS M3A78-EM NTD 2750
    2. CPU: AMD 4850e (45W 低耗電) NTD 2250
    3. RAM: DDR2-800 2GB x 2 (實在用不到這麼多,不過看到一條 999,真的找不到理由去買 1G 的...) NTD 990 x 2
    4. POWER: SEASONIC 330W (這個也很奢侈,明明整台機器全速運轉也沒有破 100W ... 不過其它牌都不信賴... 330W 是最低的規格了) NTD 1700
    5. 搖控器: Toshiba MCE 搖控器 NTD 550
    6. 便宜的 HDMI 線 3 公尺 NTD 130
    7. 喇叭: JungleRex AF-31S NTD 3800
    8. 現有的硬碟: Hitachi 7K250 250GB SATA2 HDD
    9. 現有的舊機殼、光碟機、藍芽接收器、鍵盤滑鼠... ETC

    其它像機殼,光碟機,硬碟機,鍵盤滑鼠等等就直接用現成的...。至於 MCE 一定要的 TV CARD 我反而沒買...。一方面現在的卡我都覺的太貴了 (因為之前買過 ATI theater pro 550 的卡,一張不到一千...),再加上我很少在客廳看 LIVE TV...,就算要看也是先用 MCE 錄下來。我跟家裡大人的桌機都已經有 MCE + TV 卡了,實在不大需要再買第三張... 哈哈。

    果然裝了 HTPC 這樣才覺的這台電視真的是好物啊 :D,隨便一部 DVD 或是 MPEG4 的片子 (來源當然是牧場來的...) 看起來都很棒。扣掉奢侈品,其實一萬以內都搞定了。硬體沒啥好介紹的,網路一查一堆,總是有魔人有一堆測試報告,看都看不完,輪不到我在這邊講。除了耗電量實在很低一定要講一下之外... 其它硬體介紹我就跳過去了。耗電量實在是不錯,一開機耗電量 85 W 左右,開機完進入 VISTA 後就降到 50 ~ 55W,看片時看 DVD (MPEG2) 或是 AVI ( DIVX / MPEG4 ),耗電量輕微升到 60 ~ 65W,跑一些變態的 BENCHMARK 才會飆到 75 ~ 80W...。進入待命模式才 1 ~ 2W,真的是算省電了。

     

    那特別要講的是啥? 主要要講怎樣不靠鍵盤滑鼠操作 MCE ... :D

    不用 KB / MOUSE,那當然是用搖控器了。我買的這個我覺的很不錯,接收器插上去就可以用,什麼 DRIVER 設定都免了,VISTA X64 也一切正常。搖控器上那顆綠綠的鈕,按下去就會自動叫出 MCE。叫出 MCE 後,主要的幾個功能也都有對應的按鈕,操作起來很方便。只要可以直接在 MCE 裡播放的話,用搖控器就搞定了。現在我連放 MP3 (原本用 Media Player) 跟放照片 (Windows Live Photo Gallery) 也都改用 MCE 了... (Y) 上面那張圖就是用 MCE 放 MP3 的樣子...。

    搖控器上的電源鈕,也會直接對應到電腦的 "SLEEP" 及喚醒 SLEEP 中的電腦。如果你都只用 MCE,其實操作就很簡單了。按下搖控器的開關,電腦就醒來。看完再按一下電腦就 SLEEP 去了。康博的 TV CARD 使用者比較幸福,卡片上可以直接對應到電腦的電源開關。不過... 開機關機就要等的比較久。算算 SLEEP 模式下耗掉 1W 左右的電,就算了... 隨時想看一按就開還是比較爽啊..

     

    另一個比較特別的也要講一下。過去曾貼過一篇 [一萬多塊錢的簡報器],提到有軟體 (Salling Clicker for Windows 3.5)可以當簡報器,搖控 PPT 播放,也能搖控 Media Player ...,現在這些東西有內建在 VISTA 的版本了,主要就是透過 SlideShow。在 Vista 推出一年多之後,Microsoft 總算推出 Preview 版的軟體,可以讓你的 Windows Mobile 5 / 6 的手機及PDA,搖身一變成為 Vista SlideShow Device。再搭配上 Microsoft 才開發到 BETA 版的 SlideShow Gadget for MCE... 耶! 手機可以變成 MCE 搖控器了!

    image image

    這跟 MCE 搖控器有什麼不一樣? 這篇文章有簡單的介紹... 這裡也找到另一篇...。首先,TV 的節目表都可以直接秀在手機上,播放的曲目也都會顯示在手機上,如果你不希望中斷影片的播放,這就很有用。這些功能是沒有螢幕的搖控器作不到的。不過... 大部份時間,還是搖控器好用。

    但是為什麼我還是要特別提一下? 因為... 我還要聽音樂啊,聽音樂時我就懶的開電視了。不開電視的情況下,這就很好用了,直接在手機上選好媒體櫃裡的專輯,按下播放,整個客廳就有音樂可以聽了 :D,這完全是搖控器辦不到的 :D

     

    Microsoft Vista Slideshow Media Centre Remote

    不過這兩種搖控方式不能合二為一嗎? 有... 只要肯花錢就有。這支搖控器就是符合 vista slideshow device 規格的 MCE 搖控器,一隻要 USD 199... 看到價錢,我還是買隻十分之一不到的搖控器 + 手機就好...

    有打算用 HTPC 又有 WM5/WM6 手機的人一定要試一下。反正通通是現成的啊 (除了藍芽接收器你可能得花個一百塊買一支之外)。如果你也是 HTPC 一族的,有啥 TIPS 記得提供一下,如果不是的話,快點去敗吧 (H)!

    2008/08/25 敗家 有的沒的

  3. 世紀末軟體革命復刻版

    http://www.books.com.tw/exep/prod/booksfile.php?item=0010334718

     

    沒想到電腦書也流行復古... 真是令人懷念的一本書啊,電腦書通常都是半年到一年就過時了,堆了兩年以上的書很少還會拿出來翻... 不過這本 (原版,不是復刻版) 例外,足足有好幾年的時間我還是會沒事就拿起來翻一翻。每想到十來年後還會有書局再版... 一定要推一下。

    這本書的觀念寫的實在不錯,我有很多OOP的觀念都是靠這本書建立起來的。常常有人問我學物件導向要看那本書,我都很想推這一本,不過早就絕版了那買的到... 前陣子才發現竟然有復刻版... :D

    再版的這本書是把原本上下兩冊併在一起,不知道有沒有附當年作者自己寫的 GUI 範例程式? 老實說那個範例程式在當年是蠻猛的,一張磁片就放的下的程式 (還包含 source code),一個像 windows 3.1 的視窗環境,該有的功能一點都不少,你還可以用它的 API 開發在那個 GUI 環境下的應用程式...,不過那個東西實際的用途不大,現在搞不好也沒有環境能跑了,不過看到有人可以用 C++ 建立起像 Windows 3.1 那樣的環境,而且真的可以跑,還真是打從心底佩服...。

    技術可能都退流行了,現在流行的東西書上都沒提到,不過觀念的部份還是很實用,想加強自己物件導向觀念的人不要錯過這本書... [Y]

    2008/08/23 技術隨筆 敗家 物件導向

  4. Tips: 踢掉遠端桌面連線的使用者

    近日同事連不進 SERVER,因為連線人數已滿,又摸不到本機,正在那邊苦惱...。原來大家都知道怎麼連,但是都不知道怎麼砍人... 。從 windows 2000 開始就有 RDP 可以用了,當時學到的一個指令一直到現在都可以用,就藉這個機會貼一下。

    什麼秘技都一樣,說穿了就不值錢。半年前貼了一篇 [遠端桌面連線的小技巧],裡面講到加上 /console 這參數就能連到 console session,不會跟其它人去搶那兩個連線,就可以把不順眼的 USER 砍了。連進去後要砍人很簡單,工作管理員叫出來,最後一頁 [USER] 就會列出有多少人掛在上面...

    image

    通常這樣就能解決 90% 的問題了。如果連這個秘密連線都被用掉了,那只剩另一招: TSDISCON.exe

     

    image

    TSDISCON: Disconnects a terminal session. 讚! 就是要這種東西... 用法很簡單,如果你有遠端 SERVER 的管理者權限,防火牆又沒把 NETBIOS 關掉,那麼可以這樣用:

     

    1. 先登入遠端 server
      NET USE \\MYSERVER /user:MYACCOUNT *
    2. 踢掉其它人
      TSDISCON 1 /SERVER:MYSERVER

    BINGO,其中要注意一下就是 SESSION ID,也就是上面工作管理員 ID 那一欄。0 代表 console,其它就是額外的連線。不過除非你有另外買 LICENCE,否則 OS 內建的授權只有兩個連線,意思就是亂猜一通,1 跟 2 隨便挑一個砍了就好...

    指令成功的話,被你挑中的連線就會中斷了。趁對方還沒重新連上去之前,快點連進去佔名額吧 :D

    2008/08/23 Tips 有的沒的

  5. Thread Sync #2. 實作篇 - 互相等待的兩個執行緒

    上篇有人跟我講太深奧了... Orz, 其實不會,只是還沒看到 Code 而以...。就先來幫黑暗魔人賽說明一下程式碼...。首先來看的是黑暗大魔王: GameHost..

     

    GameHost 呼叫 Player 的片段[copy code]
        public void Start(Player p)    {        // 略...        int[] guess = p.StartGuess(_maxNum, _digits);        // 略...        Hint hint = compare(guess);        // 略...        while (hint.A != _digits)        {            // 略...            guess = p.GuessNext(hint);            // 略...            hint = compare(guess);        }        p.Stop();        // 略...    }
    
       1:  public void Start(Player p)
    
       2:  {
    
       3:      // 略...
    
       4:      int[] guess = p.StartGuess(_maxNum, _digits);
    
       5:      // 略...
    
       6:      Hint hint = compare(guess);
    
       7:      // 略...
    
       8:      while (hint.A != _digits)
    
       9:      {
    
      10:          // 略...
    
      11:          guess = p.GuessNext(hint);
    
      12:          // 略...
    
      13:          hint = compare(guess);
    
      14:      }
    
      15:      p.Stop();
    
      16:      // 略...
    
      17:  }
    

     

    這段程式完全是老闆的角度在思考。抓到 PLAYER 後就叫它開始猜 StartGuess(),然後拼命的叫 PLAYER 再猜 GuessNext(), 直到猜中才可以休息 Stop()

    很典型的多型 ( Polymorphism ) 應用,實際上會 RUN 什麼 CODE,就看繼承 PLAYER 的人是怎麼寫的...。這次我們再從弱勢勞工的角度來看看 PLAYER 該怎麼實作 (以 darkthread 附的 DummyPlayer 為例):

     

    Player 實作的範例 ( DummyPlayer )[copy code]
    public class DummyPlayer : Player{    private int[] _currAnswer = null;    private Random _rnd = new Random();    private void randomGuess()    {        List<int> lst = new List<int>();        for (int i = 0; i < _digits; i++)        {            int r = _rnd.Next(_maxNum);            while (lst.Contains(r))                r = _rnd.Next(_maxNum);            lst.Add(r);            _currAnswer[i] = r;        }    }    public override int[] StartGuess(int maxNum, int digits)    {        base.StartGuess(maxNum, digits);        _currAnswer = new int[digits];        randomGuess();        return _currAnswer;    }    public override int[] GuessNext(Hint lastHint)    {        randomGuess();        return _currAnswer;    }}
    
       1:  public class DummyPlayer : Player
    
       2:  {
    
       3:      private int[] _currAnswer = null;
    
       4:      private Random _rnd = new Random();
    
       5:   
    
       6:      private void randomGuess()
    
       7:      {
    
       8:          List<int> lst = new List<int>();
    
       9:          for (int i = 0; i < _digits; i++)
    
      10:          {
    
      11:              int r = _rnd.Next(_maxNum);
    
      12:              while (lst.Contains(r))
    
      13:                  r = _rnd.Next(_maxNum);
    
      14:              lst.Add(r);
    
      15:              _currAnswer[i] = r;
    
      16:          }
    
      17:      }
    
      18:   
    
      19:      public override int[] StartGuess(int maxNum, int digits)
    
      20:      {
    
      21:          base.StartGuess(maxNum, digits);
    
      22:          _currAnswer = new int[digits];
    
      23:          randomGuess();
    
      24:          return _currAnswer;
    
      25:      }
    
      26:      public override int[] GuessNext(Hint lastHint)
    
      27:      {
    
      28:          randomGuess();
    
      29:          return _currAnswer;
    
      30:      }
    
      31:  }
    

     

    因為 CODE 不多,我就不刪了,全文照貼。另一個原因是我想讓各位看看拆成好幾段的 CODE 是不是還能夠一眼就還原成原來的邏輯?  如果只看這段 CODE 十秒鐘,沒有看註解或說明,誰能馬上回答這段 CODE 解題的邏輯是什麼?

     

    別誤會,不是指這 CODE 不易讀,而是因為呼叫的方式邏輯被迫配合 GameHost 而被切散了,你得再重新把它拼湊起來。它的邏輯很簡單,甚至簡單到連問題的答案都被忽略掉了,不過就每次都隨機丟個數字回去,在 StartGuess( ) 及 GuessNext( ) 都是。

     

     

    可憐的勞動階級要站起來啊~ 先幻想一下,如果勞工 (PLAYER) 才是老闆,那麼程式可以改成怎麼樣? 這也才是我們本篇的主角。先來看看成果再回頭來看怎麼實作。這次看的是修改後的版本: AsyncDummyPlayer.

     

    換 PLAYER 的角度思考的邏輯: AsyncDummyPlayer[copy code]
        public class AsyncDummyPlayer : AsyncPlayer    {        private int[] _currAnswer = null;        private Random _rnd = new Random();        private void randomGuess()        {            List<int> lst = new List<int>();            for (int i = 0; i < _digits; i++)            {                int r = _rnd.Next(_maxNum);                while (lst.Contains(r))                    r = _rnd.Next(_maxNum);                lst.Add(r);                _currAnswer[i] = r;            }        }        protected override void Init(int maxNum, int digits)        {            _currAnswer = new int[digits];        }        protected override void Think()        {            while (true)            {                this.randomGuess();                Hint h = this.GameHost_AskQuestion(this._currAnswer);                if (h.A == this._digits) break;            }        }    }
    
       1:  public class AsyncDummyPlayer : AsyncPlayer
    
       2:  {
    
       3:      private int[] _currAnswer = null;
    
       4:      private Random _rnd = new Random();
    
       5:      private void randomGuess()
    
       6:      {
    
       7:          List<int> lst = new List<int>();
    
       8:          for (int i = 0; i < _digits; i++)
    
       9:          {
    
      10:              int r = _rnd.Next(_maxNum);
    
      11:              while (lst.Contains(r))
    
      12:                  r = _rnd.Next(_maxNum);
    
      13:              lst.Add(r);
    
      14:              _currAnswer[i] = r;
    
      15:          }
    
      16:      }
    
      17:      protected override void Init(int maxNum, int digits)
    
      18:      {
    
      19:          _currAnswer = new int[digits];
    
      20:      }
    
      21:      protected override void Think()
    
      22:      {
    
      23:          while (true)
    
      24:          {
    
      25:              this.randomGuess();
    
      26:              Hint h = this.GameHost_AskQuestion(this._currAnswer);
    
      27:              if (h.A == this._digits) break;
    
      28:          }
    
      29:      }
    
      30:  }
    

     

    程式碼也沒比較少,都差不多。不過是那堆 CODE 換個地方擺而以。但是仔細看看,這個版本的邏輯清楚多了,PLAYER 一開始就是執行 Init( ) 的部份,而 GameHost 叫 Player 開始解題時, Player 就開始思考 (Think),而這個無腦的 Player 也很直接,就一直執行 while (true) { .... } 這個無窮迴圈,直到亂猜猜中為止。

    如果 Player 在思考的時,不管在那裡它都可以適時的呼叫 GameHost_AskQuestion( .... ) 來跟 GameHost 問答案。什麼時後該猜數字? 該猜什麼數字? 這正是整個 Player 的核心,也就是 "怎麼猜" 這件事。以人的思考方式一定會分階段,比如一開始先把所有數字猜一輪,有個概念後再想想怎麼猜能更逼近答案,最後才是致命的一擊,找出正確答案送出去,贏得比賽。

    這樣的作法,如果套在 DummyPlayer (原版本),每個階段都要塞在一個大的 switch case, 放在 GuessNext( ) 裡。而現在是那個階段? 只能靠 instance variable 了,先存著等到下次又被呼叫時再拿出來回想一下,上回作到那...。

    而第二個版本,則完全沒這個問題,就把它當作一般程式思考就夠了,第一階段就是一個 LOOP,有它自己用的一些變數。第一階段處理完畢就離開 LOOP 繼續執行後面的 CODE... 直到最後離開 Think( ) 這個 method (認輸) 或是猜中答案光榮返鄉...。

     

     

    兩者的差別看出來了嗎? DummyPlayer 像是被動的勞工,老闆說一動他就作一動。第一動作完就拿個筆計記下來,等著下次老闆再叫他,他就翻翻筆記看看之前做到那,這次繼續...。

    而 AsyncDummyPlayer 這個主動的勞工呢? 老闆交待給他一件任務後,他就自己思考起該怎麼做了。中間都不需要老闆下令。反而是過程中勞工需要老闆的協助時,老闆再適時伸出援手就可以了,一切雜務都由這位主動優秀的勞工自己處理掉。

     

     

     

    有沒有差這麼多? 這麼神奇? 是怎麼辦到的? 先來看看類別關系圖:

    ThreadSync

     

    上圖中,AsyncPlayer 就是改變這種型態的關鍵類別。AsyncPlayer 會用我們在上一篇講到的關念,化被動為主動,轉換這兩種呼叫模式。先來看看這個類別的程式碼到底變了什麼把戲,可以讓弱勢的勞工也有自主的權力?

     

     

     

     

    AsyncPlayer 實作: 化被動為主動[copy code]
        public abstract class AsyncPlayer : Player    {        public override int[] StartGuess(int maxNum, int digits)        {            base.StartGuess(maxNum, digits);            Thread thinkThread = new Thread(this.ThinkCaller);            thinkThread.Start();                        this._host_return.WaitOne();            return this._temp_number;        }        public override int[] GuessNext(Hint lastHint)        {            this._temp_hint = lastHint;            this._host_call.Set();            this._host_return.WaitOne();            return this._temp_number;        }        public override void Stop()        {            base.Stop();            this._temp_hint = new Hint(this._digits, 0);            this._host_call.Set();            this._host_end.WaitOne();            this._host_complete = true;        }        private void ThinkCaller()        {            try            {                this.Init(this._maxNum, this._digits);                this.Think();            }            catch (Exception ex)            {                Console.WriteLine("Player Exception: {0}", ex);            }            finally            {                this._host_end.Set();            }        }        protected abstract void Init(int maxNum, int digits);        protected abstract void Think();        private AutoResetEvent _host_call = new AutoResetEvent(false);        private AutoResetEvent _host_return = new AutoResetEvent(false);        private AutoResetEvent _host_end = new AutoResetEvent(false);        private bool _host_complete = false;        private int[] _temp_number;        private Hint _temp_hint;        protected Hint GameHost_AskQuestion(int[] number)        {            if (this._host_complete == true) throw new InvalidOperationException("GameHost stopped!");            lock (this)            {                try                {                    this._temp_number = number;                    this._host_return.Set();                    this._host_call.WaitOne();                    return this._temp_hint;                }                finally {                    this._temp_number = null;                    this._temp_hint = new Hint(-1, -1);                }            }        }    }
    
       1:  public abstract class AsyncPlayer : Player
    
       2:  {
    
       3:      public override int[] StartGuess(int maxNum, int digits)
    
       4:      {
    
       5:          base.StartGuess(maxNum, digits);
    
       6:          Thread thinkThread = new Thread(this.ThinkCaller);
    
       7:          thinkThread.Start();
    
       8:          this._host_return.WaitOne();
    
       9:          return this._temp_number;
    
      10:      }
    
      11:      public override int[] GuessNext(Hint lastHint)
    
      12:      {
    
      13:          this._temp_hint = lastHint;
    
      14:          this._host_call.Set();
    
      15:          this._host_return.WaitOne();
    
      16:          return this._temp_number;
    
      17:      }
    
      18:      public override void Stop()
    
      19:      {
    
      20:          base.Stop();
    
      21:          this._temp_hint = new Hint(this._digits, 0);
    
      22:          this._host_call.Set();
    
      23:          this._host_end.WaitOne();
    
      24:          this._host_complete = true;
    
      25:      }
    
      26:      private void ThinkCaller()
    
      27:      {
    
      28:          try
    
      29:          {
    
      30:              this.Init(this._maxNum, this._digits);
    
      31:              this.Think();
    
      32:          }
    
      33:          catch (Exception ex)
    
      34:          {
    
      35:              Console.WriteLine("Player Exception: {0}", ex);
    
      36:          }
    
      37:          finally
    
      38:          {
    
      39:              this._host_end.Set();
    
      40:          }
    
      41:      }
    
      42:      protected abstract void Init(int maxNum, int digits);
    
      43:      protected abstract void Think();
    
      44:      private AutoResetEvent _host_call = new AutoResetEvent(false);
    
      45:      private AutoResetEvent _host_return = new AutoResetEvent(false);
    
      46:      private AutoResetEvent _host_end = new AutoResetEvent(false);
    
      47:      private bool _host_complete = false;
    
      48:      private int[] _temp_number;
    
      49:      private Hint _temp_hint;
    
      50:      protected Hint GameHost_AskQuestion(int[] number)
    
      51:      {
    
      52:          if (this._host_complete == true) throw new InvalidOperationException("GameHost stopped!");
    
      53:          lock (this)
    
      54:          {
    
      55:              try
    
      56:              {
    
      57:                  this._temp_number = number;
    
      58:                  this._host_return.Set();
    
      59:                  this._host_call.WaitOne();
    
      60:                  return this._temp_hint;
    
      61:              }
    
      62:              finally {
    
      63:                  this._temp_number = null;
    
      64:                  this._temp_hint = new Hint(-1, -1);
    
      65:              }
    
      66:          }
    
      67:      }
    
      68:  }
    

     

    這段程式碼長了一點,內容也都刪不得,各位請耐心點看。上一篇我畫了張概念性的時序圖,這次我們再拿同一張圖,不過這次會標上程式碼:

     ThreadSync2

     

     

    請注意一下各個箭頭的上下順序。由上往下代表時間的進行,如果應該在後面執行的 CODE 不巧先被呼叫了,則動作較快的那個 THREAD 會被迫暫停,等待另一邊的進度跟上。先來看看 StartGuess( ) 怎麼跟 Think( ) 互動:

    StartGuess(...)[copy code]
            public override int[] StartGuess(int maxNum, int digits)        {            base.StartGuess(maxNum, digits);            Thread thinkThread = new Thread(this.ThinkCaller);            thinkThread.Start();                        this._host_return.WaitOne();            return this._temp_number;        }
    
       1:  public override int[] StartGuess(int maxNum, int digits)
    
       2:  {
    
       3:      base.StartGuess(maxNum, digits);
    
       4:      Thread thinkThread = new Thread(this.ThinkCaller);
    
       5:      thinkThread.Start();
    
       6:      this._host_return.WaitOne();
    
       7:      return this._temp_number;
    
       8:  }
    

     

    GameHost 呼叫 Player.StartGuess( ) 有兩個目的,一個是給 Player 題目範圍,讓 Player 做好準備動作。另一個則是準備好之後 GameHost 要取得 Player 傳回的第一個問題。

    程式碼很忠實的做了一樣的事,只不過 StartGuess( ) 建立了新的執行緒來負責。新的執行緒會執行 ThinkCaller( ),啟動之後 GameHost 這邊就什麼都不作,等待 _host_return 這個 WaitHandle 被叫醒,代表另一邊已經做好了,可以從共用變數 _temp_number 取得問題傳回去。

    既然 GameHost 在等待某人通知它,我們就來看看是誰會通知他題目已經準備好了:

     

    GameHost_AskQuestion(...)[copy code]
            protected Hint GameHost_AskQuestion(int[] number)        {            if (this._host_complete == true) throw new InvalidOperationException("GameHost stopped!");            lock (this)            {                try                {                    this._temp_number = number;                    this._host_return.Set();                    this._host_call.WaitOne();                    return this._temp_hint;                }                finally {                    this._temp_number = null;                    this._temp_hint = new Hint(-1, -1);                }            }        }
    
       1:  protected Hint GameHost_AskQuestion(int[] number)
    
       2:  {
    
       3:      if (this._host_complete == true) throw new InvalidOperationException("GameHost stopped!");
    
       4:      lock (this)
    
       5:      {
    
       6:          try
    
       7:          {
    
       8:              this._temp_number = number;
    
       9:              this._host_return.Set();
    
      10:              this._host_call.WaitOne();
    
      11:              return this._temp_hint;
    
      12:          }
    
      13:          finally {
    
      14:              this._temp_number = null;
    
      15:              this._temp_hint = new Hint(-1, -1);
    
      16:          }
    
      17:      }
    
      18:  }
    

     

    就在 GameHost 正在等題目的時後,另一個執行緒正在進行 "思考" 的動作,直到有結論後會呼叫 GameHost_AskQuestion( ... ) 送出問題。這時這個問題會被放到 _temp_number, 而下一步就是 _host_return.Set( ), 通知另一個執行緒,正在等這個結果的人: "喂! 東西已經準備好了,可以來取貨了!!"

    整個機制就這樣串起來了。GameHost 那邊怎麼把答案傳回來? 同樣的作法,反過來而以。GameHost 會藉著呼叫 Player.GuessNext(...) 把答案傳回來,而這時就觸動一樣的機制,讓另一邊 Player Thread 呼叫的 GameHost_AskQuestion( ... ) 醒過來,把答案拿走, RETURN 回去。

    這樣一直重複下去,剩下最後一個同步的點,就是結束遊戲的地方。說穿了也是一樣的把戲,只是這次是藉著 GameHost 呼叫 Player.Stop( ),而另一邊 Player Thread 執行完 Think( ) 後,兩邊就一起結束遊戲了。

     

    總算講完了。其實 thread 能解決的問題還真是五花八門。每次當我想出這些方法來簡化問題時,我就會覺的很有成就感。雖然寫出這個不會讓我贏得比賽,反而因為同步的關係,AsyncDummyPlayer 執行的速度還遠遠落後 DummyPlayer (我的機器跑起來,大概差了四~五倍 ...) 。不過我知道我簡單的頭腦,不先把問題簡化的話,我大概解決不了太複雜的問題...。也許是缺了這種能力,才讓我更有動力去想簡化問題的方式吧?

     

    最後,為什麼每次講到 thread 的文章,都是 code 一點點,文章跟圖一大堆? 咳咳... 難道我也到了靠一張嘴混日子的地步了嘛? Orz... 本系列到此結束,以後還會有什麼主題? 想到再說啦~~ 今天各位記得去拜拜~~ 下回見!

    2008/08/15 系列文章: 多執行緒的處理技巧 .NET Tips 作業系統 多執行緒 技術隨筆 物件導向