繼上篇,有人跟我講太深奧了... Orz, 其實不會,只是還沒看到 Code 而以...。就先來幫黑暗魔人賽說明一下程式碼...。首先來看的是黑暗大魔王: GameHost..
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 為例):
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.
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 這個主動的勞工呢? 老闆交待給他一件任務後,他就自己思考起該怎麼做了。中間都不需要老闆下令。反而是過程中勞工需要老闆的協助時,老闆再適時伸出援手就可以了,一切雜務都由這位主動優秀的勞工自己處理掉。
有沒有差這麼多? 這麼神奇? 是怎麼辦到的? 先來看看類別關系圖:
上圖中,AsyncPlayer 就是改變這種型態的關鍵類別。AsyncPlayer 會用我們在上一篇講到的關念,化被動為主動,轉換這兩種呼叫模式。先來看看這個類別的程式碼到底變了什麼把戲,可以讓弱勢的勞工也有自主的權力?
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: }
這段程式碼長了一點,內容也都刪不得,各位請耐心點看。上一篇我畫了張概念性的時序圖,這次我們再拿同一張圖,不過這次會標上程式碼:
請注意一下各個箭頭的上下順序。由上往下代表時間的進行,如果應該在後面執行的 CODE 不巧先被呼叫了,則動作較快的那個 THREAD 會被迫暫停,等待另一邊的進度跟上。先來看看 StartGuess( ) 怎麼跟 Think( ) 互動:
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 在等待某人通知它,我們就來看看是誰會通知他題目已經準備好了:
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... 本系列到此結束,以後還會有什麼主題? 想到再說啦~~ 今天各位記得去拜拜~~ 下回見!
之前因為被盜文,除了被弄的不大爽之外,倒也沒什麼事,不過有人給我個 link,無意間發現對岸竟然有這麼篇文章...
(最原始的網頁不知為何被下架了,底下提供的 LINK 是目前還連的到的...)
URL: http://www.cnblogs.com/hullfqaz/archive/2008/07/28/1254315.html
看了又好氣又好笑... 沒想到對岸有網友在討論我碰到的事件啊,底下還真的有人回應批評百度...。前同事 在 MSN 跟我打屁,說這篇引用我的事例來討論的網站,也一樣沒有經過我的同意就轉貼,我的言論可能會引起兩岸對立,引發第三次世界大戰... Orz...
這次好一點,雖然也是原文照貼我的文章,至少是全文照貼,頭尾都留著,LINK 也留著,沒有很 "自動" 的翻成簡體中文...後來好奇用相關關鑑字再去 GOOGLE 找看看,多找到一篇:
URL: http://blog.const.net.cn/news/20080728/ec3b4ba924bde2b4.htm
不過這次運氣差了點,我的 IP 被封鎖不能看... 我應該沒有偉大到讓對方直接封我的 IP 吧?
不過人氣好像不大夠,搜了半天才搜到這兩篇... 看來要上頭條新聞還久的很.. 哈哈..
別以為我轉行了... 這篇不是勵志文章,教你用主動積極的態度面對人生.... 而是討論執行緒同步機制及如何用來解決惱人的流程問題。會寫這篇的念頭來自黑暗程式魔人辦的猜數字程式設計大賽,在處理的過程中想到的解法...,不過這篇要講的不是猜數字,而是不相干的東西: Thread Sync (執行緒的同步機制)。
一般程式寫久了,會很習慣一路到底的思考方式,程式也完全照這樣的思路被設計出來。不過寫 GAME 這類的程式就不是這麼一回事了。就先舉十五年前我用 C 寫的俄羅斯方塊的遊戲當例子 (大驚! 十... 十五年?),腦袋裡想的流程八九不離十,一定是像這樣:
"隨機從上面掉一個方塊下來,時間到了就往下掉,USER有按方向鍵就左右移動或是旋轉,直到卡到底下或是其它方塊為止..."
很正確的想法,很可惜你的程式完全不能這樣寫,為什麼? 當你沒有使用多執行緒或是其它的技巧時,你的主程式流程一定得被限定在固定時間 refresh 畫面的無窮迴圈... 上面的邏輯怎麼辦? 會被迫拆成好幾塊,然後被主程式定期呼叫... 程式寫起來大概會像這樣:
1: public void ProcessBrick()
2: {
3: switch (status)
4: {
5: case 1:
6: //
7: // 按右鍵, 往右移一格
8: //
9: break;
10: case 2:
11: //
12: // 按左鍵, 往左移一格
13: //
14: break;
15: case 3:
16: //
17: // 按上鍵, 順時針旋轉 90 度
18: //
19: break;
20: case 4:
21: //
22: // 按下鍵, 往下移一格
23: //
24: break;
25: case 5:
26: //
27: // .......
28: //
29: break;
30: }
31: }
原本好好的邏輯被切成好幾塊,然後再藉著狀態等資訊,每次的 LOOP 各挑這次要執行的那一小段,然後拼湊出原本的邏輯...。別哀怨,誰叫你寫的 CODE 不是老大? 老大是控制畫面的主程式,你既然是當小的就乖乖躲在旁邊被呼叫... 委屈一點是應該的...。
真是黑心啊... 誰叫老闆永遠是對的。這次黑心... 不,黑暗魔人出的題目正好又讓我聯想到一樣的狀況。黑暗魔人出的題目,是先實作了 GameHost 類別 (就是老大啦),及 Player 抽象型別 (小角色就是他),再藉著多型 (Polymorphism) 的方式,由 GameHost 不斷的呼叫 Player 提供的 GuessNum( ),來讓 Player 問問題,同時把上一次問題的答案傳給 Player ...
程式也不難寫,大家都玩過 1A1B 的遊戲吧? 腦袋裡一定是這樣想的:
"一開始先隨便猜幾個,把結果記下來....。"
"再來刪去法,比較可能的幾個數字再猜一猜...。"
"快猜到了,幾種組合列出來想一想怎麼辦好? 好! 就猜這個...。"
...
...
...
"BINGO! 猜中了!!!"
很高興的想好流程後,真的要開始寫就傻住了... 這堆流程跟邏輯,要我拆成一直會被重複呼叫的 "單一個" method, 每一回合會被呼叫一次...。意思是一連串複雜的處理過程,要依序切成完全一樣的片段 (就是指重複呼叫同一段程式碼) ? 更慘的是這次問的問題,下一次呼叫才拿的到答案!? MY GOD... 要想出怎麼猜到對方的數字已經夠頭痛了,還得來處理這些 "行政" 問題?
My God, 頭越想越痛,這樣下去來我大概只能寫出比 DummyPlayer (註: 參賽程式附的 Player 範例,隨機產生問題,無腦的一直問,直到猜中為止...) 高明一點點的程式而已了。曾 MSN 跟黑暗魔人討論過,看看能不能讓 GameHost 化主動為被動,改由 GameHost 提供 callback 讓 Player 呼叫的可能性? 不過後來想想不對,那有主持人在旁邊看等著被來賓訪問的道理? 何況如果以後是兩個 Player 對戰怎麼辦? 誰要來當 "小的" ?? 問題還是一樣沒解決...
過去大家對於 THREAD 的印象都是 "多工",需要用 thread 解決的問題大多是在改善執行效能上面,因為同時用兩個 thread 可以做兩件事,效率會比較好。其實 thread 也很適合解決這類的問題,因為執行緒讓我們有機會,不需切斷 GameHost / Player 的 "思路",讓兩邊都能用很直覺的思考方式寫程式。
大概畫一下時序圖對照一下,先看看原本的作法:
再看一下改用兩個執行緒的作法:
不管之中的技術障礙怎麼克服,至少這樣改起來,兩邊都能各自用更合理簡單的方式思考自己的問題,也更接近實際的情況 (莊家跟玩家不會共用一個腦子吧...)。也唯有把問題簡化之後,我們才有辦法想出更複雜的方式來解決問題,科技不就是這樣進步的嘛?
這次的例子裡,執行緒是用來簡化問題的,而不是拿來增進效率的。兩個人腦袋各自想著問題,總要溝通吧? thread 之間溝通的機制就很單純了,共用變數加上同步機制,來確定對方是否準備好我要的東西,或是對方是否已經準備好要接招了?
這次搬出來的是過去說明 thread pool 提到的 AutoResetEvent,現在又重現江湖了。方法很簡單,要拿資料的那一方,就去 Wait( ) 等資料準備好,另一方把資料放在共用變數之後就呼叫 Set( ),叫醒另一個還在 Wait( ) 的執行緒,可以起床拿東西閃人了...。
接下來當然就各忙各的,直到雙方又有交換資料的需求,同樣的方式就再來一次。只是隨著資料交換的方向不同 (比如問問題是把題目由 player --> host,而取得答案則是 host --> player),上述的動作雙方角色要互換才能順利進行。
Orz,本來想一篇打完的,不過打到快睡著,加上內容還剩不少... 就分兩集吧! 程式碼及實作各位就耐心點,下一篇會端出實際操作執行緒的範例。
以前 (古早以前) 寫過一個簡單的 LIBRARY,就是去抓現在連上網頁的 CLIENT IP,然後簡單的套上 NET MASK,看看是不是在指定的網段內? 是的話就作些特別的處理 blah blah... 原本的 code 有點雜,我精簡之後變這樣,如果是 192.168.2.0 / 24 這範圍內的使用者連到這網頁,就會顯示 "Is Intranet? YES" ... 夠簡單吧? (怎麼連幾篇都這種不入流的 sample code ...)
這段 code 一直都運作的很好,沒碰過什麼大問題,不過就是把 IP address 切成四個 bytes, 然後利用位元運算併成 unsing integer, 方便跟後面的 netmask 作 bits and ...。不過某日興沖沖裝好 vista x64 + IIS7 之後發現,程式竟然不動了!?
先來看一下原始碼:
1: <%@ Page Language="C#" Trace="true" %>
2: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3: <script runat="server">
4:
5: protected void Page_Load(object sender, EventArgs e)
6: {
7: this.Trace.Warn(System.Net.IPAddress.Parse(this.Request["REMOTE_HOST"]).AddressFamily.ToString());
8: this.IPLabel.Text = this.IsInSubNetwork(
9: "192.168.2.0",
10: "255.255.255.0",
11: this.Request.ServerVariables["REMOTE_HOST"]) ? ("YES") : ("NO");
12: }
13:
14:
15: private bool IsInSubNetwork(string network, string mask, string address)
16: {
17: uint netval = _IP2INT(network);
18: uint maskval = _IP2INT(mask);
19: uint addval = _IP2INT(address);
20:
21: return (netval & maskval) == (addval & maskval);
22: }
23:
24: private uint _IP2INT(string address)
25: {
26: string[] segments = address.Split('.');
27:
28: uint ipval = 0;
29: foreach (string segment in segments)
30: {
31: ipval = ipval * 256 + uint.Parse(segment);
32: }
33:
34: return ipval;
35: }
36:
37:
38: </script>
39:
40: <html xmlns="http://www.w3.org/1999/xhtml">
41: <head runat="server">
42: <title>Untitled Page</title>
43: </head>
44: <body>
45: <form id="form1" runat="server">
46: <div>
47: Is Intranet? <asp:Label ID="IPLabel" runat="server" />
48: </div>
49: </form>
50: </body>
51: </html>
後來追了半天才意外發現問題出在這... 打開 ASP.NET Trace, 看一下 REMOTE_ADDR 到底抓到啥子東西?
嘖嘖嘖,搞半天原來是 Vista 預設把 IPv6 給開了起來,IIS7 / DevWeb 都中獎,直接回報 IPv6 格式的 IP Address 回來... 怎麼解? 這種問題說穿了就不值錢,強迫用 IPv4 就好。我試過幾種可行的方式,有:
碰到這種怪問題,一時之間還熊熊不知道是那裡掛掉,還真是麻煩... 特地記一下這篇,讓一樣吃過 IPv6 苦頭的人參考一下。至於怎樣作才對? 當然是用 "正規" 的方式來處理 IP Address... System.Net.IPAddress 類別包含一個靜態方法: IPAddress Parse(string ipaddress), 用它可以把字串格式的 IP 換成這個類別的 instance, 用它內建的 property: AddressFamily,看看值是 enum 型態的 InterNetwork 還是 InterNetworkV6 就知道了,不要像我當年年少不更事一樣,自己硬去拆字串... Orz
今天的範例超簡單,簡單到很沒水準的地步... 難道本 columns 的水準降低了嘛? 咳咳... 不多說,今天的例子也不需要解釋,直接來看 sample code:
1: using System;
2: using System.Data;
3: using System.Web.UI.WebControls;
4: using System.Data.Odbc;
5:
6: public partial class _Default : System.Web.UI.Page
7: {
8: protected void Page_Load(object sender, EventArgs e)
9: {
10: DataSet ds = new DataSet();
11: OdbcConnection conn = new OdbcConnection("Driver={Microsoft Text Driver (*.txt; *.csv)};DBQ=" + Server.MapPath("~/App_Data"));
12: OdbcDataAdapter adpt = new OdbcDataAdapter("select * from [database.txt]", conn);
13: adpt.Fill(ds);
14:
15: this.DataGrid1.DataSource = ds.Tables[0];
16: this.DataGrid1.DataBind();
17: }
18: }
真的是沒什麼特別的 code, 連 exception 都沒處理... 難道 .aspx 有什麼特別的嘛? 來看看:
1: <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
2: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3: <html xmlns="http://www.w3.org/1999/xhtml">
4: <head runat="server">
5: <title>Untitled Page</title>
6: </head>
7: <body>
8: <form id="form1" runat="server">
9: <div>
10: <asp:DataGrid ID="DataGrid1" runat="server" />
11: </div>
12: </form>
13: </body>
14: </html>
真的沒啥特別的,再來看看 CSV 檔的內容好了,看看有沒有什麼特別的...
1: name,email
2: chicken,chicken@chicken-house.net
3: peter,peter@chicken-house.net
4: annie,annie@chicken-house.net
5: nancy,nancy@chicken-house.net
咳,想扁我的忍一下... 這支程式大概只比 "Hello World" 好一點,會看本 BLOG 的大概用看的就知道結果是什麼了吧? 不就把所有的內容套到 DataGrid 裡顯示出來? 像這樣:
真是沒營養的內容... 現在要開始進入主題了。執行環境是 Vista x64 + Visual Studio 2008,這個 web site 是透過 DevWeb 來執行的,不是透過 IIS... 反正都一樣嘛,測完可以 RUN (這種程式應該不會有什麼 BUG,頂多打錯字編譯錯誤..) 後就收工了,把它 DEPLOY 到 IIS 上面 ( windows 2003 x64, IIS6 ) 跑看看:
掛了... 當然... 不然這篇是要討論什麼? 老實說這是我親身碰到的例子,從這錯誤訊息還真摸不著頭腦,完全搞不懂發生什麼事。該裝的都裝了,也都沒錯,為什麼會這樣? 二話不說,先確定系統的 ODBC 是正常的,最好的方式就是找現成的程式試看看,就可以初步判定是我的問題 OR 系統的問題。到控制台的 ODBC Data Source Administrator 看看,先建個同樣的 ODBC data source...
真是見鬼了,我的 windows 95 vpc 能用的 odbc driver 都比這裡多... 看來真的是沒有 ODBC driver,那我裝的 ADODB 是裝到那裡去了?
科學的實驗都講求先假設,再控制變因,然後證明假設是正確的... 不過現在一點線索都沒有,只能靠運氣了。會有這篇也真的是運氣好,聯想到是不是 x64 的問題? 用我謹有的知識: x64 / x86 兩種模式的程式不能同時出現在單一 process, 為了證明這件事,就特地在 SERVER 安裝了 excel, 用 excel 來開啟 odbc, 竟然可以?
解這問題,裝 office 是一年多前的事了,現在也沒畫面好抓,就跳過去... 想到 x64 一堆東西都有兩種版本,控制台是不是也有? 真該好好拿箱仙草蜜,拜一拜交大門口的土地公... Bingo! 執行了32位元版的 ODBC Data source Administrator (c:\Windows\SysWOW64\odbccp32.cpl), 結果出現了這畫面:
真是好狗運,如果沒矇對的話,不知道還要搞多久... 這時才恍然大悟,原來 x64 要求所有的 driver 都要是 64 位元版,加上單一 process 不能混用 x86 / x64 兩種模式的 code,就是指這個... driver 不只是 "硬體" 的 driver,連各種軟體的都算。廣義一點來說,ODBC driver, OleDB Provider 也都算 driver 的一種,各種 Plug-Ins,甚至是各種 COM 元件 (只要是 In-Process 的都算),到 COM 的延伸... IE ActiveX Control,Media Player 用的 Codec ... 通通都算...
我終於體會到要轉移到 x64 有多麻煩了。在 DOS 年代或是 WIN 3.1 年代,每個軟體都很獨立,換到 WIN32 沒什麼問題。現在的軟體就不一樣,轉到 x64 都可以跑,不過要用到的各種共用元件就不一定了。拿掉 COM 的話,VB / ASP 大概就什麼都不剩了吧...
回題,來看看這問題怎麼解。雖然搞清楚原因,但是我的程式還是不能動。CSV 其實還可以用文字檔硬解,不過我實際工作上碰到的例子是要解讀上傳的 EXCEL 檔的內容... EXCEL 我可沒辦法硬搞... 不過現在方向清楚了,只要有辦法把程式從 64 位元模式改成 32 位元模式執行,就可以抓的到 32 位元模式下的 ODBC Data Source, 程式就正常了。不過該怎麼告訴 IIS6,我的程式需要的執行環境是 32 位元?
上網查了一下 x64 版的 IIS 如何執行 x86 模式的程式? 找到這篇:
http://support.microsoft.com/kb/894435/zh-tw
IIS 6.0 同時支援 32 位元模式及 64 位元模式。但是,IIS 6.0 不支援同時在 64 位元版的 Windows 上執行兩種模式。ASP.NET 1.1 只能在 32 位元模式中執行。ASP.NET 2.0 可以在 32 位元模式或 64 位元模式中執行。因此,如果要同時執行 ASP.NET 1.1 和 ASP.NET 2.0,您必須在 32 位元模式中執行 IIS。
實際切換的動作在這篇也有寫...
ASP.NET 2.0 的 32 位元版本
如果要執行 32 位元版的 ASP.NET 2.0,請依照下列步驟執行: 1. 按一下 [開始],再按一下 [執行],輸入 cmd,然後按一下 [確定]。
2. 輸入下列命令以啟用 32 位元模式:
cscript %SYSTEMDRIVE%\inetpub\adminscripts\adsutil.vbs SET W3SVC/AppPools/Enable32bitAppOnWin64 1
3. 輸入下列命令以安裝 ASP.NET 2.0 (32 位元) 的版本,以及在 IIS 根目錄和下列位置底下安裝指令碼對應:
%SYSTEMROOT%\Microsoft.NET\Framework\v2.0.40607\aspnet_regiis.exe -i
4. 請確定在 Internet Information Services Manager 的 Web Service Extension 清單中,將 ASP.NET 2.0.40607 版 (32 位元) 的狀態設定為 Allowed。
切換過後,再重新執行一次,一切就正常了:
雖然在 x64 下執行 x86 的程式,也是有一堆額外的好處,不過看起來就是不大爽... IIS6 只能二選一,兩種模式只能挑一種。這個問題到了 IIS7 就獲得解決了。 IIS7 允許同時存在這兩種模式的 Application ..
其實在 x64 下的問題還很多,不過大都不外乎這模式,x64 / x86 的 dll 不能混用。現今軟體都用一堆元件,你得確保每一個用到的元件都有 x64 版,如果有一個沒有? 乖乖的切回 x86 來執行吧...。類似的小狀況其實還蠻多的,下回多列幾種我碰到的狀況,以免各位跟我一樣碰釘子還試個老半天... 敬請期帶第三集 :D