討論區快速選單
知識庫快速選單
討論區最近新進100則主題 掌握Salesforce雲端管理秘訣
[ 回上頁 ] [ 討論區發言規則 ]
WaitForSingleObject的用法?
更改我的閱讀文章字型大小
作者 : coldworld(man)
[ 貼文 43 | 人氣 6125 | 評價 0 | 評價/貼文 0 | 送出評價 10 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/9 下午 08:48:26
我有一個Dialog-based的程式,上面有兩個buttons,一個負責開始一個Worker Thread,一個負責取消的動作...

void CmyDlg::OnButtonStartThread()
{
/* Both myFlag and myThreadObjPtr are the class members of CmyDlg */
myFlag = 1;
myThreadObjPtr = AfxBeginThread(&myThread,this, THREAD_PRIORITY_NORMAL, 0, 0, NULL);
}


Uint CmyDlg::myThread(LPvoid lpv)
{
// TODO: Add your control notification handler code here
CmyDlg*dlg = (CmyDlg*)lpv;
     while(dlg->myFlag)
     {
     :
     :
     }
}

void CmyDlg::OnButtonCancel()
{
     myFlag = 0;
     WaitForSingleObject(myThreadObjPtr ->m_hThread,INFINITE);
}

目前的狀況是只要一進入WaitForSingleObject,程式就死在堶情A請問我以上的程式架構有錯嗎?
作者 : sflam(Raymond)討論區板主 Visual C++ .NET卓越專家VC++一代宗師新手入門優秀好手資訊類作業求救頂尖高手C++一代宗師貼文超過4000則
[ 貼文 4945 | 人氣 9172 | 評價 32290 | 評價/貼文 6.53 | 送出評價 142 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/9 下午 11:51:40
看 WaitForSingleObject() 的說明, 那個 handle 必須要有 SYNCHRONIZE 的 access right. 這好像牽涉到 security descriptor, 且只能用在 NT 級的系統.

作者 : quickwolf(疾風之狼) 貼文超過200則
[ 貼文 258 | 人氣 1837 | 評價 1420 | 評價/貼文 5.5 | 送出評價 11 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/10 上午 08:33:21
覺得問題出在:
myFlag = 0;

while(dlg->myFlag)
{
   :
}

那個 dlg->myFlag, myFlag = 0; 並非atomic operation,
如果沒有將 myFlag 宣告成volatile的話, 編譯器很可能會在執行期產生
變數的副本且放置在CPU暫存器中,當執行 myFlag = 0;時,很可能只是將暫存器中的副本
設為0, 於是dlg->myFlag也就取不到正確值.. 或者於變數存取時發生context switch
而造成預期之外的結果..

這個Case最簡單的解決之道是用
InterlockedIncrement() / InterlockedDecrement() 這類API來修改myFlag
防止修改變數途中發生context switch

另外建議在 WaitForSingleObject 設個timeout這樣就不會死當
必要時方便後續處理..



作者 : sflam(Raymond)討論區板主 Visual C++ .NET卓越專家VC++一代宗師新手入門優秀好手資訊類作業求救頂尖高手C++一代宗師貼文超過4000則
[ 貼文 4945 | 人氣 9172 | 評價 32290 | 評價/貼文 6.53 | 送出評價 142 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/10 下午 09:27:05
有可能好像是 quickwolf(疾風之狼) 所講的. 研究用 event 來替代 myFlag 吧. 大概這樣:

- CmyDlg 裡加成員 CEvent m_evCancel;

- 在 OnButtonStartThread() 裡在產生 thread 之前先 reset event.
   m_evCancel.ResetEvent();
   AfxBeginThread(...)


- 在 myThread() 裡測試該 event:
   while (::WaitForSingleObject(dlg->m_evCancel, 0) == WAIT_TIMEOUT)
   {
     ...
   }
 並在 myThread() 終結時把 myThreadObjPtr 設為 0:
   dlg->myThreadObjPtr = NULL;

- 在 OnButtonCancel() 裡 set event:
   m_evCancel.SetEvent();

 SetEvent() 後, 有幾個方法來測試 thread 是否已結束:
   用 GetExitCodeThread(), 看 dwExitCode 是不是 STILL_ALIVE.
   檢查 myThreadObjPtr 是否 (在 myThread() 內) 設為 NULL.
   用 WaitForSingleObject() 應該是沒有問題的. (我先前的覆文有考慮不周的地方)

作者 : coldworld(man)
[ 貼文 43 | 人氣 6125 | 評價 0 | 評價/貼文 0 | 送出評價 10 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/10 下午 09:57:50
我覺得應該不是atomic或context switch的問題...因為如果是這樣的話,程式的執行結果應該呈現"不穩定"的狀況(有時當有時不當),而不是"每次都當",我有試過加Sleep(),試著改變程式執行順序,結果依然是"每次"都死在WaitForSingleObject....

照著你的方法改用CEvent,還是沒用耶....依舊當在SetEvent()那一行堶...
(還是很感謝你)

另外,如果thread handle已經被我們設為NULL,應該不能再呼叫GetExitCodeThread
了吧?
作者 : shing819(Clier) VC++曠世奇才貼文超過1000則人氣指數超過30000點
[ 貼文 1740 | 人氣 40353 | 評價 8630 | 評價/貼文 4.96 | 送出評價 84 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/10 下午 11:34:16

你原來程式竟然可以在你電腦上編譯,真奇怪?我改成我電腦
能跑的程式.


修改:

public:
BOOL myFlag;
CWinThread *myThreadObjPtr;

/////////////////////////////////////////////////////////////

(1)
Uint myThread(LPvoid lpv)
{
CmyDlg*dlg = (CmyDlg*)lpv;
while(dlg->myFlag)
{

}
return 0;
}


(2)
myThreadObjPtr = AfxBeginThread(myThread,this,
THREAD_PRIORITY_NORMAL, 0, 0, NULL);


我測試後確定應該不是myFlag,這種寫法,不太正規,
所以樓上的先進才會判斷是myFlag.
作者 : sflam(Raymond)討論區板主 Visual C++ .NET卓越專家VC++一代宗師新手入門優秀好手資訊類作業求救頂尖高手C++一代宗師貼文超過4000則
[ 貼文 4945 | 人氣 9172 | 評價 32290 | 評價/貼文 6.53 | 送出評價 142 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/11 上午 12:49:50
>我覺得應該不是atomic或context switch的問題...因為如果是這樣的話,程式的執行結果應該呈現'不穩定'的狀況(有時當有時不當),而不是'每次都當',我有試過加Sleep(),試著改變程式執行順序,結果依然是'每次'都死在WaitForSingleObject....

請澄清一下, 是程式 crash 還是 hang 在 WaitForSingleObject() .


>照著你的方法改用CEvent,還是沒用耶....依舊當在SetEvent()那一行堶...
>(還是很感謝你)

在 myThread 堻]一個 breakpoint, debug 看看.

你在 myThread 埵釣S有用 MFC 的物件來存取 dialog's item?

把 myThread 堶 while-loop 做的東西貼出來.


>另外,如果thread handle已經被我們設為NULL,應該不能再呼叫GetExitCodeThread
>了吧?

在 SetEvent() 前就要把 thread 的 HANDLE 先存起來.
  HANDLE hThread = myThreadObjPtr->m_hThread;
  m_evCancel.SetEvent();
  DWORD dwExitCode;
  ::GetExitCodeThread(hWatchThread, &dwExitCode);

作者 : quickwolf(疾風之狼) 貼文超過200則
[ 貼文 258 | 人氣 1837 | 評價 1420 | 評價/貼文 5.5 | 送出評價 11 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/11 上午 09:33:33
去調查了一下編譯出來的碼
Debug版:

; 40 : myFlag=0;
; 41 : WaitForSingleObject(m_pThread->m_hThread,INFINITE);

mov eax, DWORD PTR [ecx+104]
mov DWORD PTR [ecx+96], 0 <== myFlag=0
push -1
mov ecx, DWORD PTR [eax+44]
push ecx
call DWORD PTR __imp__WaitForSingleObject@8


Release版:
; 40 : myFlag=0;

mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax+96], 0 <== myFlag=0

; 41 : WaitForSingleObject(m_pThread->m_hThread,INFINITE);

由List可以看出這個case的myFlag雖然不是嚴謹的用法
但問題並非出在這兒.. (用VC++預設的編譯設定)
而且仿照你的法子去用,Thread可以正常結束
建議照sflam大大說的加一些除錯訊息,以方便追查問題..

Uint CmyDlg::myThread(LPvoid lpv)
{
TRACE("-- WorkerThread Start --\n"); <=看一下thread是否正常啟動

CmyDlg*dlg = (CmyDlg*)lpv;
    while(dlg->myFlag)
    {
     : <== 把這部分弄出來研究一下!!
    }
    
    TRACE("-- WorkerThread Stop --\n"); <=看一下thread是否結束
    return 0;
}
作者 : coldworld(man)
[ 貼文 43 | 人氣 6125 | 評價 0 | 評價/貼文 0 | 送出評價 10 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/11 上午 10:49:34
我目前是按照大家的幫忙改成這樣...
void CmyDlg::OnButtonStartThread()
{
/* myThreadObjPtr is a member of CmyDlg */
/*m_evtBegin(CEvent) is a member of CmyDlg */

m_evtBegin.ResetEvent();
myThreadObjPtr = AfxBeginThread(&myThread,this, THREAD_PRIORITY_NORMAL, 0, 0, NULL);
}

Uint CmyDlg::myThread(LPvoid lpv)
{
  CmyDlg*dlg = (CmyDlg*)lpv;
   int jump_out = 0;
   DWORD temp;
   for(i=0;i<FILE_LENGTH;) //main loop of this thread...
   {
     temp = ::WaitForSingleObject(dlg->m_evtBegin, 0);
     if(temp != WAIT_TIMEOUT)
     {
     jump_out = 1;
     break;
     }
     :
     :
    }
LAB1:
    if(jump_out)
    {
     myDBGMSG("Access thread is stopped...");
     ::AfxEndThread(0);
return 0;
     }
     :
     :
}

void CmyDlg::OnButtonCancel()
{
     HANDLE hThread = myThreadObjPtr ->m_hThread;
     m_evtBegin.SetEvent();
     DWORD dwExitCode;
     do{
    ::GetExitCodeThread(hThread, &dwExitCode);
    }while(dwExitCode == STILL_ACTIVE);

LAB2:
   DoCancelTask();
}

在OnButtonCancel()中,我加Do-while的目的是希望LAB1先跑,然後再跑LAB2
但目前的狀況是按下Cancel按鈕後,程式Hang在以上的do-while堶...

如果把do-while拿掉,則dwExitCode依然拿到259 (STILL_ACTIVE)
程式執行順序是先跑LAB2再跑LAB1...(違反我的需求)
作者 : coldworld(man)
[ 貼文 43 | 人氣 6125 | 評價 0 | 評價/貼文 0 | 送出評價 10 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/11 上午 10:54:29
void CmyDlg::OnButtonCancel()
{
     HANDLE hThread = myThreadObjPtr ->m_hThread;
     m_evtBegin.SetEvent();
     DWORD dwExitCode;
     do{
    ::GetExitCodeThread(hThread, &dwExitCode);
    }while(dwExitCode == STILL_ACTIVE);

LAB2:
   DoCancelTask();
}

其中hThread真的只要執行
HANDLE hThread = myThreadObjPtr ->m_hThread;
就可拿到嗎?
是否要改為DuplicateHandle () ?
作者 : quickwolf(疾風之狼) 貼文超過200則
[ 貼文 258 | 人氣 1837 | 評價 1420 | 評價/貼文 5.5 | 送出評價 11 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/11 下午 12:29:45
更正一下小弟之前的說法: "myFlag雖然不是嚴謹的用法但問題並非出在這兒"
因為那時並未詳細測試Release版, Debug版是可以執行,但Release版會掛
若是將 myFlag 宣告為volatile就可以正常脫離..

原本 int myFlag;
myFlag=0 這行在Release版會生成:
mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax+96], 0

改成 volatile int myFlag;
myFlag=0 這行在Release版會生成:
mov DWORD PTR [ecx+96], 0

發現跟Debug版生出來的碼是相同的(未最佳化) ^^
因此context switch還是會造成問題!!
嚴格來說 myFlag 不僅必須宣告 volatile
且 myFlag=0; 必須改用 InterlockedDecrement( (LPlong volatile)&myFlag )

覺得樓主把事情弄得複雜了.. 提供小弟的測試碼希望能有所助益:


// TestThreadDlg.cpp : implementation file
//

#include <afxmt.h>
#include "stdafx.h"
#include "TestThread.h"
#include "TestThreadDlg.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef this_FILE
static char this_FILE[] = __FILE__;
#endif


Uint CTestThreadDlg::WorkerThread(LPvoid lpParam)
{
TRACE("-- WorkThread Start --\n");

CTestThreadDlg *dlg=(CTestThreadDlg *)lpParam;
dlg->m_Count=0;

/*
while(dlg->myFlag) {
dlg->m_Count++;
}
*/
while( WAIT_OBJECT_0!=WaitForSingleObject(dlg->evQuit,0) ) {
dlg->m_Count++;
}

TRACE("-- WorkThread Quit --\n");
return 0;
}

void CTestThreadDlg::OnButtonStart()
{
//myFlag=1;
//InterlockedIncrement( (LPlong volatile)&myFlag )
evQuit.ResetEvent();
m_pThread=AfxBeginThread(WorkerThread, (LPvoid)this);
}

void CTestThreadDlg::OnButtonStop()
{
//myFlag=0;
//InterlockedDecrement( (LPlong volatile)&myFlag );
evQuit.SetEvent();
WaitForSingleObject(m_pThread->m_hThread,INFINITE);

TRACE("-- Job Done --\n");
}
作者 : coldworld(man)
[ 貼文 43 | 人氣 6125 | 評價 0 | 評價/貼文 0 | 送出評價 10 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/11 下午 01:11:14

>void CTestThreadDlg::OnButtonStop()
>{
> //myFlag=0;
> //InterlockedDecrement( (LPlong volatile)&myFlag );
> evQuit.SetEvent();
> WaitForSingleObject(m_pThread->m_hThread,INFINITE);
>
> TRACE('-- Job Done --');
>}

感謝你的幫忙與測試,我依照你的架構來修改我的code,測試結果依然是hang在
WaitForSingleObject(m_pThread->m_hThread,INFINITE);這一行跳不出來...
把這一行拿掉就沒事,但卻會造成TRACE('-- Job Done --');的程式碼被提前執行...(WorkerThread還沒結束就執行)
作者 : quickwolf(疾風之狼) 貼文超過200則
[ 貼文 258 | 人氣 1837 | 評價 1420 | 評價/貼文 5.5 | 送出評價 11 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/11 下午 05:09:39
加 WaitForSingleObject(m_pThread->m_hThread,INFINITE);
的目的就是要等待WorkerThraed結束,然後才進行後面的動作.. 沒有理由把他拿掉><

會卡住多半是因為WorkerThraed沒有結束的關係..
而且很有可能跟WorkerThraed裡面作的事有關,可以的話還是把WorkThread
的內容清楚交代一下..

擅用 TRACE/OutputDebugString 之類的除錯訊息
可以有效掌控程式執行流程.. 必須確認Thraed是否正常開始?
是否收到evQuit的觸發? Thraed是否正常結束?
有時候Thraed裡面邏輯的錯誤也可能照成收到觸發後仍無法結束的情形
因此在Thread裡到處加一大堆TRACE亦是不錯的方法..

另一個法子就是先撰寫簡單測試程式,就像上面提供的那樣
確定能執行後再慢慢加入實際要用的功能
剛開始不要求快,穩扎穩打才是上策

作者 : coldworld(man)
[ 貼文 43 | 人氣 6125 | 評價 0 | 評價/貼文 0 | 送出評價 10 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/11 下午 05:56:29

>加 WaitForSingleObject(m_pThread->m_hThread,INFINITE);
>的目的就是要等待WorkerThraed結束,然後才進行後面的動作.. 沒有理由把他拿掉><
>
>會卡住多半是因為WorkerThraed沒有結束的關係..
>而且很有可能跟WorkerThraed裡面作的事有關,可以的話還是把WorkThread
>的內容清楚交代一下..
>
>擅用 TRACE/OutputDebugString 之類的除錯訊息
>可以有效掌控程式執行流程.. 必須確認Thraed是否正常開始?
>是否收到evQuit的觸發? Thraed是否正常結束?
>有時候Thraed裡面邏輯的錯誤也可能照成收到觸發後仍無法結束的情形
>因此在Thread裡到處加一大堆TRACE亦是不錯的方法..
>
>另一個法子就是先撰寫簡單測試程式,就像上面提供的那樣
>確定能執行後再慢慢加入實際要用的功能
>剛開始不要求快,穩扎穩打才是上策
>
>

把WaitForSingleObject(m_pThread->m_hThread,INFINITE);拿掉時,
WorkerThread的確會被中斷跳出(SetEvent()的確有觸發evQuit)...
所以觸發機制沒有問題,跟我的WorkerThread內的while迴圈內做了什麼事應該沒有關係...
從我自己印出來的debug message來看,我發現
進入WaitForSingleObject函式後,CPU就再也沒有進入WorkerThread了!!!!!!
作者 : sflam(Raymond)討論區板主 Visual C++ .NET卓越專家VC++一代宗師新手入門優秀好手資訊類作業求救頂尖高手C++一代宗師貼文超過4000則
[ 貼文 4945 | 人氣 9172 | 評價 32290 | 評價/貼文 6.53 | 送出評價 142 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/11 下午 10:17:58
把你 myThread() 堜狾陸答F西的程式碼都 comment 掉, 單單剩下一個用來檢查 event 的 loop, 或是用來檢查 myFlag 的 loop:

  Uint CmyDlg::myThread(LPVLID lpv)
  {
    CmyDlg *pthis = (CMyDlg*)lpv;
    Uint i = 0;
    while (::WaitForSingleObject(pthis->m_evtBegin, 0) != WAIT_OBJECT_0)
    {
      ::Sleep(250);
      TRACE("%d\n", i++);
    }
    TRACE("Thread End\n");
    return 0;
  }

或把上面的 while() 變成:
  while (dlg->myFlag)


我的測試碼:
  void CmyDlg::OnBtnStart()
  {
   m_evtCancel.ResetEvent();
   m_pThread = ::AfxBeginThread(myThread, (LPvoid)this);
  }

  void CmyDlg::OnBtnCancel()
  {
   HANDLE hThread = m_pThread->m_hThread;
   m_evtCancel.SetEvent();
   DWORD dwWait = WaitForSingleObject(hThread, INFINITE);
   TRACE("continue...\n");
  }

m_evtCancel 與 m_pThread 是 CmyDlg 的成員, 類型分別是 CEvent 跟 CWinThread*.



>其中hThread真的只要執行
>HANDLE hThread = myThreadObjPtr ->m_hThread;
>就可拿到嗎?

是的

>是否要改為DuplicateHandle () ?

應該是不需要.
作者 : coldworld(man)
[ 貼文 43 | 人氣 6125 | 評價 0 | 評價/貼文 0 | 送出評價 10 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/12 上午 09:02:41
嗯...照著你的code把所有程式碼都拿掉,只檢查event,還是一樣...
在OnBtnCancel()中,會hang在WaitForSingleObject(hThread, INFINITE);
而從我自己的debug訊息來看,CPU再也沒有執行myThread了...
如果把這行拿掉(WaitFroSingleObject),就可以正常跳出while迴圈(所以SetEvent的確有產生作用),但執行順序就變成不是我要的....:(


>把你 myThread() 堜狾陸答F西的程式碼都 comment 掉, 單單剩下一個用來檢查 event 的 loop, 或是用來檢查 myFlag 的 loop:
>
>  Uint CmyDlg::myThread(LPVLID lpv)
>  {
>    CmyDlg *pthis = (CMyDlg*)lpv;
>    Uint i = 0;
>    while (::WaitForSingleObject(pthis->m_evtBegin, 0) != WAIT_OBJECT_0)
>    {
>      ::Sleep(250);
>      TRACE('%d
', i++);
>    }
>    TRACE('Thread End
');
>    return 0;
>  }
>
>或把上面的 while() 變成:
>  while (dlg->myFlag)
>
>
>我的測試碼:
>  void CmyDlg::OnBtnStart()
>  {
>   m_evtCancel.ResetEvent();
>   m_pThread = ::AfxBeginThread(myThread, (LPvoid)this);
>  }
>
>  void CmyDlg::OnBtnCancel()
>  {
>   HANDLE hThread = m_pThread->m_hThread;
>   m_evtCancel.SetEvent();
>   DWORD dwWait = WaitForSingleObject(hThread, INFINITE);
>   TRACE('continue...
');
>  }
>
>m_evtCancel 與 m_pThread 是 CmyDlg 的成員, 類型分別是 CEvent 跟 CWinThread*.
>
>
>
>>其中hThread真的只要執行
>>HANDLE hThread = myThreadObjPtr ->m_hThread;
>>就可拿到嗎?
>
>是的
>
>>是否要改為DuplicateHandle () ?
>
>應該是不需要.
>
作者 : quickwolf(疾風之狼) 貼文超過200則
[ 貼文 258 | 人氣 1837 | 評價 1420 | 評價/貼文 5.5 | 送出評價 11 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/12 上午 11:01:53
WaitForSingleObject(hThread, INFINITE);

DWORD rc=WaitForSingleObject(hThread, 5000); // 斟酌設一下timeout
switch( rc ) {
  case WAIT_OBJECT_0: TRACE("return WAIT_OBJECT_0\n"); break;
  case WAIT_ABANDONED: TRACE("return WAIT_ABANDONED\n"); break;
  case WAIT_TIMEOUT: TRACE("return WAIT_TIMEOUT\n"); break;
  default:
     TRACE("return Undefined\n");
}

然後看一下是call WaitForSingleObject 之後是完全卡住
還是會timeout跳出, 然後看傳回值..

但依照你的描述,猜測會完全卡住.. 這時建議你重開個Project用VC++的預設值試試
也可找別台電腦試一下.. 小弟之前用的測試碼試過是可以正常執行的
(用VC++ 6.0 MFC Wizard產生的Dialog based 全部都是用預設值)
如果這樣還不行者就沒招了..><|||




作者 : quickwolf(疾風之狼) 貼文超過200則
[ 貼文 258 | 人氣 1837 | 評價 1420 | 評價/貼文 5.5 | 送出評價 11 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/12 下午 01:07:12
想起一種可能性: 由於AfxBeginThread() 生出來的thread有autoDelete的特性
亦即當WorkerThread結束之後會自動將相關的handle,分配的資源刪除
所以利用Event通知WorkerThread結束然後再用WaitForSingleObject()去等WorkerThread的handle觸發,這樣的做法會有問題 =.=

evQuit.SetEvent();
Sleep(2000); // 中間等一會可以將問題突顯出來..
DWORD rc=WaitForSingleObject(m_pThread->m_hThread,5000);
switch( rc ) {
  case WAIT_OBJECT_0: TRACE("return WAIT_OBJECT_0\n"); break;
  case WAIT_ABANDONED: TRACE("return WAIT_ABANDONED\n"); break;
  case WAIT_TIMEOUT: TRACE("return WAIT_TIMEOUT\n"); break;
  default:
TRACE("return Undefined\n");
}

不過這樣的現象在我的電腦上只是傳回錯誤,並不會有卡住的現象..
要修改autoDelete的屬性,書上教的方法必須對CWinThread作subclassing
覺得有些麻煩,建議改這樣看是否可以滿足你的需求:

Uint CTestThreadDlg::WorkerThread(LPvoid lpParam)
{
  CTestThreadDlg *dlg=(CTestThreadDlg *)lpParam;
  while( WAIT_OBJECT_0!=WaitForSingleObject(dlg->evQuit,0) ) {
    dlg->m_Count++;
  }
  
  dlg->evQuitAck.SetEvent();
  return 0;
}

void CTestThreadDlg::OnButtonStop()
{
  evQuitAck.ResetEvent();
  evQuit.SetEvent();

  Sleep(2000); // 加這個只是跟原本用法做個比較並無特別用意
  //DWORD rc=WaitForSingleObject(m_pThread->m_hThread,INFINITE);
  DWORD rc=WaitForSingleObject(evQuitAck,INFINITE);
}

這樣改成等待evQuitAck,不要等WorkerThread的handle就可以避開autoDelete
造成的困擾..
作者 : quickwolf(疾風之狼) 貼文超過200則
[ 貼文 258 | 人氣 1837 | 評價 1420 | 評價/貼文 5.5 | 送出評價 11 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/12 下午 01:42:48
剛沒仔細看書.. 其實有簡單的修改方法:

void CTestThreadDlg::OnButtonStart()
{
  evQuit.ResetEvent();
  m_pThread=AfxBeginThread(WorkerThread, (LPvoid)this, THREAD_PRIORITY_NORMAL,0,CREATE_SUSPENDED, NULL);
  m_pThread->m_bautoDelete=false;
  m_pThread->ResumeThread();
}

void CTestThreadDlg::OnButtonStop()
{
  evQuit.SetEvent();
  Sleep(2000); // 突顯auto delete問題
  DWORD rc=WaitForSingleObject(m_pThread->m_hThread,INFINITE);
  delete m_pThread->m_hThread; // 因為m_bautoDelete已設為false,所以要自行善後

}
作者 : coldworld(man)
[ 貼文 43 | 人氣 6125 | 評價 0 | 評價/貼文 0 | 送出評價 10 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/12 下午 05:05:35
加evQuitAck及設定m_bautoDelete這個方式...執行結果都一樣..卡在WaitForSingleObject堙A如果設timeout,則回傳WAIT_TIMEOUT...
看來真要另用一個Project來試看看了...T_T
作者 : sflam(Raymond)討論區板主 Visual C++ .NET卓越專家VC++一代宗師新手入門優秀好手資訊類作業求救頂尖高手C++一代宗師貼文超過4000則
[ 貼文 4945 | 人氣 9172 | 評價 32290 | 評價/貼文 6.53 | 送出評價 142 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/12 下午 09:54:10
>嗯...照著你的code把所有程式碼都拿掉,只檢查event,還是一樣...

Output window 埵閉搢 "Thread End" 嗎?

在 thread return 0 的地方放一個斷點, 並在 WaitForSingleObject(hThread, INFINITE) 的地方放一個斷點.

當你 OnButtonStop() 後 debugger 會不會停在第一個斷點?

還有一個想法: 加多一個 event, 在 thread 結束 return 前去設它, 然後你在 OnButtonStop() 那裡去等這個 event, 不要去等 thread.

作者 : coldworld(man)
[ 貼文 43 | 人氣 6125 | 評價 0 | 評價/貼文 0 | 送出評價 10 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/13 上午 07:58:36

>>嗯...照著你的code把所有程式碼都拿掉,只檢查event,還是一樣...
>
>Output window 埵閉搢 'Thread End' 嗎?
>
>在 thread return 0 的地方放一個斷點, 並在 WaitForSingleObject(hThread, INFINITE) 的地方放一個斷點.
>
>當你 OnButtonStop() 後 debugger 會不會停在第一個斷點?
>

不會....因為一進入WaitForSingleObject(hThread, INFINITE) 後..就hang在堶惜F...
而程式也再也沒切換到WorkerThread去了...

>還有一個想法: 加多一個 event, 在 thread 結束 return 前去設它, 然後你在 OnButtonStop() 那裡去等這個 event, 不要去等 thread.
>
>
上三篇的quickwolf兄已經這樣建議了....還是一樣...會hang在等這個新加的event的
WaitForSingleObject...:(
作者 : sflam(Raymond)討論區板主 Visual C++ .NET卓越專家VC++一代宗師新手入門優秀好手資訊類作業求救頂尖高手C++一代宗師貼文超過4000則
[ 貼文 4945 | 人氣 9172 | 評價 32290 | 評價/貼文 6.53 | 送出評價 142 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/13 下午 10:21:51
你還沒有回答到底 thread 有沒有結束.

- 在 thread 的一開始放一個斷點 (斷點一).
- 在 thread 結束的 return 前加 TRACE("Thread End\n");
- 在 thread , 把 WaitForSingleObject() 的值存起來.
- 在 TRACE() 這裡也加一個斷點 (斷點二).

按 button 來跑 thread, debugger 會停在斷點一. 先 single step 幾圈.

當按 button 來結束 thread 時, debugger 因該會停在上面 TRACE() 的斷點 (斷點二). Single step 到 thread return.

如果沒有, 可能你的 thread 沒有結束. 在 thread WaitForSingleObject()的地方加一個斷點 (斷點三). Debugger 應該會停在這裡. Single Step, 檢查看 WaitForSingleObject() 傳回的值.

把你完整的程式碼貼上來.

作者 : coldworld(man)
[ 貼文 43 | 人氣 6125 | 評價 0 | 評價/貼文 0 | 送出評價 10 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/16 上午 08:25:42

>你還沒有回答到底 thread 有沒有結束.
>
>- 在 thread 的一開始放一個斷點 (斷點一).
>- 在 thread 結束的 return 前加 TRACE('Thread End
');
>- 在 thread , 把 WaitForSingleObject() 的值存起來.
>- 在 TRACE() 這裡也加一個斷點 (斷點二).
>
>按 button 來跑 thread, debugger 會停在斷點一. 先 single step 幾圈.
>
>當按 button 來結束 thread 時, debugger 因該會停在上面 TRACE() 的斷點 (斷點二). Single step 到 thread return.
>
問題就出在這..."當按 button 來結束 thread 時",程式停在OnButtonCancel()的WaitForSingleObject堶探N不出來了

>如果沒有, 可能你的 thread 沒有結束. 在 thread WaitForSingleObject()的地方加一個斷點 (斷點三). Debugger 應該會停在這裡. Single Step, 檢查看 WaitForSingleObject() 傳回的值.
>
>把你完整的程式碼貼上來.
>
>



void CmyDlg::OnButtonPgr()
{
   m_evtBegin.ResetEvent();
  myThreadPtr = AfxBeginThread(&MyThread,this, THREAD_PRIORITY_NORMAL, 0, 0, NULL);
}

Uint CmyDlg::MyThread(LPvoid lpv)
{
int jump_out = 0;
     DWORD temp;
:
:

for(i=0;i<FILE_LENGTH;)
{
temp = WaitForSingleObject(dlg->m_evtBegin, 0);
if(temp == WAIT_OBJECT_0)
{
jump_out = 1;
myDBG_MSG("MyThread : get stop command!!"); break;
}
SomeTask();
}
if(jump_out)
{

myDBG_MSG("thread is ready to EXIT...");
::AfxEndThread(0);
return 0;
}
}

void CmyDlg::OnButtonCancel()
{
CString str;
myDBG_MSG("Canceling...");
HANDLE hThread = myThreadPtr->m_hThread;
m_evtBegin.SetEvent();
myDBG_MSG("Wait for thread terminated...");
     //wait for the thread terminated
DWORD rc=WaitForSingleObject(hThread,INFINITE);
myDlg->CancelTask();
myDBG_MSG("Send cancel command to target OK!!");
}
/*****************************************************/
按下Cancel Button會出現以下訊息
Canceling...
MyThread : get stop command!!
Wait for thread terminated...
接著程式hang在DWORD rc=WaitForSingleObject(hThread,INFINITE); 堶
/*****************************************************/
如果把DWORD rc=WaitForSingleObject(hThread,INFINITE)拿掉,
則會得到以下訊息
Canceling...
MyThread : get stop command!!
Wait for thread terminated...
Send cancel command to target OK!!
thread is ready to EXIT...

(我希望先出現thread is ready to EXIT...再出現Send cancel command to target OK!!
)
作者 : sflam(Raymond)討論區板主 Visual C++ .NET卓越專家VC++一代宗師新手入門優秀好手資訊類作業求救頂尖高手C++一代宗師貼文超過4000則
[ 貼文 4945 | 人氣 9172 | 評價 32290 | 評價/貼文 6.53 | 送出評價 142 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/17 上午 12:49:30
>void CmyDlg::OnButtonPgr()
>{
> m_evtBegin.ResetEvent();
> myThreadPtr = AfxBeginThread(&MyThread,this, THREAD_PRIORITY_NORMAL, 0, 0, NULL);
>}
>
>Uint CmyDlg::MyThread(LPvoid lpv)
>{
> int jump_out = 0;
> DWORD temp;
> :
> :
>
> for(i=0;i<FILE_LENGTH;)
> {
> temp = WaitForSingleObject(dlg->m_evtBegin, 0);
> if(temp == WAIT_OBJECT_0)
> {
> jump_out = 1;
> myDBG_MSG("MyThread : get stop command!!");
> break;
> }
> SomeTask();
> }
> if(jump_out)

上面的 break 就會跳出 for-loop 了, 為什麼還需要 'jump_out'?

> {
>
> myDBG_MSG("thread is ready to EXIT...");
> ::AfxEndThread(0);

AfxEndThread() 在這裡是多餘的.

> return 0;
> }

雖然你上面的程式碼沒有顯示出來, 但如果 'i' 有改變以致 for-loop 終結, 那上面這個 if 內的程式就不會執行, 那你傳回什麼值?

還有一點, 如果你 for-loop 結束了, 那 thread 也就結束了. OnButtonCancel() 堛 WaitForSingleObject() 是會傳回 WAIT_FAILED. (在 VC++2005 測試了, 確是如此).


  Uint CmyDlg::MyThread(LPvoid lpv)
  {
    CmyDlg *dlg = (CmyDlg*)lpv;
    for (...)
    {
      if (::WaitForSingleObject(dlg->m_evtBegin, 0) == WAIT_OBJECT_0)
      {
        break;
      }
      SomeTask();
    }

    myDBG_MSG("thread is ready to EXIT...");
    return 0;
  }

>}
>
>void CmyDlg::OnButtonCancel()
>{

[刪...]

>}
>/*****************************************************/
>按下Cancel Button會出現以下訊息

[刪...]

>(我希望先出現thread is ready to EXIT...再出現Send cancel command to target OK!!
>)

不拿掉 DWORD rc=WaitForSingleObject(hThread,INFINITE), 我得到的是:

  Wait for thread terminated...
  MyThread : get stop command!!
  thread is ready to EXIT...
  The thread 'Win32 Thread' (0xf2c) has exited with code 0 (0x0).
  Send cancel command to target OK!!

但我不是用 myDGB_MSG(), 我是用 TRACE(), 並在每一行用 '\n' 來斷行.

你是在什麼系統上測試的? 我的是 XP Professional.

作者 : coldworld(man)
[ 貼文 43 | 人氣 6125 | 評價 0 | 評價/貼文 0 | 送出評價 10 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/17 上午 07:55:56

>>void CmyDlg::OnButtonPgr()
>>{
>> m_evtBegin.ResetEvent();
>> myThreadPtr = AfxBeginThread(&MyThread,this, THREAD_PRIORITY_NORMAL, 0, 0, NULL);
>>}
>>
>>Uint CmyDlg::MyThread(LPvoid lpv)
>>{
>> int jump_out = 0;
>> DWORD temp;
>> :
>> :
>>
>> for(i=0;i<FILE_LENGTH;)
>> {
>> temp = WaitForSingleObject(dlg->m_evtBegin, 0);
>> if(temp == WAIT_OBJECT_0)
>> {
>> jump_out = 1;
>> myDBG_MSG('MyThread : get stop command!!');
>> break;
>> }
>> SomeTask();
>> }
>> if(jump_out)
>
>上面的 break 就會跳出 for-loop 了, 為什麼還需要 ''jump_out''?

>
>> {
>>
>> myDBG_MSG('thread is ready to EXIT...');
>> ::AfxEndThread(0);
>
>AfxEndThread() 在這裡是多餘的.
==>嗯.原本也沒加...但已試到沒招了..所以才加上去的
>
>> return 0;
>> }
>
>雖然你上面的程式碼沒有顯示出來, 但如果 ''i'' 有改變以致 for-loop 終結, 那上面這個 if 內的程式就不會執行, 那你傳回什麼值?

==>連jump_out一起回答...事實上if(jump_out){}之後還有程式,jump_out只是為了要判斷是迴圈自然結束或使用者按下Cancel鈕...不過..不管那種case..都回傳0
>
>還有一點, 如果你 for-loop 結束了, 那 thread 也就結束了. OnButtonCancel() 堛 WaitForSingleObject() 是會傳回 WAIT_FAILED. (在 VC++2005 測試了, 確是如此).
>
>
>  Uint CmyDlg::MyThread(LPvoid lpv)
>  {
>    CmyDlg *dlg = (CmyDlg*)lpv;
>    for (...)
>    {
>      if (::WaitForSingleObject(dlg->m_evtBegin, 0) == WAIT_OBJECT_0)
>      {
>        break;
>      }
>      SomeTask();
>    }
>
>    myDBG_MSG('thread is ready to EXIT...');
>    return 0;
>  }
>
>>}
>>
>>void CmyDlg::OnButtonCancel()
>>{
>
>[刪...]
>
>>}
>>/*****************************************************/
>>按下Cancel Button會出現以下訊息
>
>[刪...]
>
>>(我希望先出現thread is ready to EXIT...再出現Send cancel command to target OK!!
>>)
>
>不拿掉 DWORD rc=WaitForSingleObject(hThread,INFINITE), 我得到的是:
>
>  Wait for thread terminated...
>  MyThread : get stop command!!
>  thread is ready to EXIT...
>  The thread ''Win32 Thread'' (0xf2c) has exited with code 0 (0x0).
>  Send cancel command to target OK!!
>
>但我不是用 myDGB_MSG(), 我是用 TRACE(), 並在每一行用 ''
'' 來斷行.
>
>你是在什麼系統上測試的? 我的是 XP Professional.
>
>
我是Window 2000
作者 : coldworld(man)
[ 貼文 43 | 人氣 6125 | 評價 0 | 評價/貼文 0 | 送出評價 10 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/17 上午 09:06:52
找到原因了...感謝你...是UpdateUI的問題...

我的prime thread如果正在WaitForSingleObject(hThread,INFINITE) 時,
而MyThread()又要求prime thread去updateUI
(mydlg->m_btn_pgr.EnableWindow(ON);)
可能就進入死結了....
之前要是把mydlg->m_btn_pgr.EnableWindow(ON)這些相關程式碼也post出來,
我想大家一定很快就解出來了...-_-:
Sorry




void CmyDlg::OnButtonPgr()
{
   m_evtBegin.ResetEvent();
  myThreadPtr = AfxBeginThread(&MyThread,this, THREAD_PRIORITY_NORMAL, 0, 0, NULL);
}

Uint CmyDlg::MyThread(LPvoid lpv)
{
     int jump_out = 0;
     DWORD temp;
     :
     :

     for(i=0;i<FILE_LENGTH;)
     {
     temp = WaitForSingleObject(dlg->m_evtBegin, 0);
     if(temp == WAIT_OBJECT_0)
     {
     jump_out = 1;
     mydlg->m_btn_pgr.EnableWindow(ON);
     //myDBG_MSG("MyThread : get stop command!!");
     break;
     }
     SomeTask();
     }
     if(jump_out)
     {
     myDBG_MSG("thread is ready to EXIT...");
     return 0;
     }
}

void CmyDlg::OnButtonCancel()
{
CString str;
myDBG_MSG("Canceling...");
HANDLE hThread = myThreadPtr->m_hThread;
m_evtBegin.SetEvent();
myDBG_MSG("Wait for thread terminated...");
     //wait for the thread terminated
DWORD rc=WaitForSingleObject(hThread,INFINITE);
myDlg->CancelTask();
myDBG_MSG("Send cancel command to target OK!!");
}
作者 : quickwolf(疾風之狼) 貼文超過200則
[ 貼文 258 | 人氣 1837 | 評價 1420 | 評價/貼文 5.5 | 送出評價 11 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2006/1/17 下午 01:34:03
改成用 MsgWaitForMultipleObjects ,等Thread結束時亦同時對Message進行處理..
像這樣:

DWORD nWaitCnt=1;
HANDLE *hWaitArray=&m_pThread->m_hThread;
DWORD rc;
do {
  rc=MsgWaitForMultipleObjects(nWaitCnt, hWaitArray, false, INFINITE, QS_ALLEVENTS);
  if( (WAIT_OBJECT_0+nWaitCnt)==rc ) { // got message
    MSG msg;
    if (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
     ::TranslateMessage(&msg);
     ::DispatchMessage(&msg);
    }
  }
}while( WAIT_OBJECT_0!=rc );
作者 : quickwolf(疾風之狼) 貼文超過200則
[ 貼文 258 | 人氣 1837 | 評價 1420 | 評價/貼文 5.5 | 送出評價 11 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
主題發起人coldworld註記此篇回應為最佳解答 2006/1/18 上午 08:47:53
另外建議還是要處理一下 auto delete問題, 否則如果AfxBeginThread產生的CWinThread
被delete之後才用WaitForSingleObject之類的函式去等會因為Handle不存在而產生錯誤..
雖然這樣WaitForSingleObject一樣會跳出...

順便更正上次的筆誤:
void CTestThreadDlg::OnButtonStop()
{
  evQuit.SetEvent();
  Sleep(2000); // 突顯auto delete問題
  DWORD rc=WaitForSingleObject(m_pThread->m_hThread,INFINITE);
  delete m_pThread->m_hThread; // 因為m_bautoDelete已設為false,所以要自行善後
}

那個 delete m_pThread->m_hThread;
要改成 delete m_pThread;
是要delete CWinThread物件才對 ^^|||

MFC的Code是這樣寫的:
當WorkerThread結束時會自行呼叫AfxEndThread()

void AFXAPI AfxEndThread(Uint nExitCode, BOOL bDelete)
{
...
if (bDelete)
  pThread->Delete();
..
}

void CWinThread::Delete()
{
  // delete thread if it is auto-deleting
  if (m_bautoDelete)
    delete this;
}

CWinThread::~CWinThread()
{
  // free thread object
  if (m_hThread != NULL)
     CloseHandle(m_hThread);
...
}

因此 m_bautoDelete設為false之後,要自行將CWinThread給delete掉,
然後CWinThread的解構函式會自己去CloseHandle,這樣就收拾乾淨了.








 板主 : 青衫 , Raymond
 > Visual C++ - 討論區
 - 最近熱門問答精華集
 - 全部歷史問答精華集
 - Visual C++ - 知識庫
  ■ 全站最新Post列表
  ■ 我的文章收藏
  ■ 我最愛的作者
  ■ 全站文章收藏排行榜
  ■ 全站最愛作者排行榜
  ■  月熱門主題
  ■  季熱門主題
  ■  熱門主題Top 20
  ■  本區Post排行榜
  ■  本區評價排行榜
  ■  全站專家名人榜
  ■  全站Post排行榜
  ■  全站評價排行榜
  ■  全站人氣排行榜
 請輸入關鍵字 
  開始搜尋
 
Top 10
評價排行
Visual C++
1 青衫 11070 
2 Raymond 10090 
3 Clier 7630 
4 小約翰 2500 
5 Cog 2030 
6 coco 1870 
7 aming 1410 
8 牧童哥 1400 
9 r2109 1380 
10 Akira 1350 
Visual C++
  專家等級 評價  
  一代宗師 10000  
  曠世奇才 5000  
  頂尖高手 3000  
  卓越專家 1500  
  優秀好手 750  
Microsoft Internet Explorer 6.0. Screen 1024x768 pixel. High Color (16 bit).
2000-2019 程式設計俱樂部 http://www.programmer-club.com.tw/
0.25