420 likes | 613 Views
請以純粹黑白列印. Thread Basics. 井民全製作. 包含 function parameters 與 Local variables. OS 用來管理 thread 使用. Introduction. 用來存放程式碼. Process consists Process kernel object + address space Thread consists Thread kernel object + thread stack Thread 執行 process ’ s address space 中的 程式碼
E N D
請以純粹黑白列印 Thread Basics 井民全製作
包含 function parameters 與 Local variables OS 用來管理 thread 使用 Introduction 用來存放程式碼 • Process consists • Process kernel object + address space • Thread consists • Thread kernel object + thread stack • Thread • 執行 process’s address space 中的程式碼 • 處理放在該 address space 中的變數 Thread 擁有 自己的 STACK
Introduction • Process 本身是靜止不動的 • 一個行程從不執行任何程式碼,它只是一個 thread 的容器 • Thread 總是在某個行程的「執行內容(context)」中被建立,並且在該行程中度過它們的一生 當 thread function 執行結束 則 thread 生命也就結束
因為 kernel object Table 以 process 為單位放置 Introduction • If you have two threads running in the context of a single process • they share the same address space • Threads can share kernel object handles • Process 因為需要獨立的 address space,故比起 thread 來說,消耗更多資源 Thread 只需一個 Kernel object 與 stack Virtual address space 需要許多的紀錄, .exe 與 .dll 也需要 file resource
A path of execution with a process 注意: 這是 primary Thread執行的流程 Process 起始 Create a primary thread C/C++ run-time Startup code void main(){ 你的程式碼 } Entry-point function main, wmain, WinMain, wWinMain Process 結束 呼叫 ExitProcess 結束
可以是任意名稱 呼叫者可以傳進 任意變數型態 盡量使用 local 變數 以免除同步的問題 Writing your First Thread Function DWORD WINAPI ThreadFunc(PVOID pvParam){ DWORD dwResult = 0; … return (dwResult); } 當 thread function return時, thread 自動消滅
0 立即執行 CREATE_SUSPENDED 等到呼叫 ResumeThread後才執行 The Create Thread Function如何建立Thread SECURITY_ATTRIBUTES sa; sa.nLength=sizeof(sa); sa.lpSecurityDescriptor=NULL; sa.bInheritHandle=TRUE; HANDLE CreateThread( PSECURITY_ATTRIBUTES psa; DWORD cbStack; PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD fdwCreate, PDWORD pdwThreadID); *NULL=> 預設 security *若你希望 ChildProcess 可以 存取這個新的 thread,則 建立新的 sa 丟進去 指定Thread 執行的程式 也就是前面的 thread fun 給定使用的堆疊大小, 0 預設值 給定使用的參數 建立的方式 回傳一個 ThreadID
馬上寫個範例 建立好 thread 後 立即執行 設定 thread fun 預設 security HANDLE hThread=CreateThread(NULL,0, FirstThread,(PVOID)Data,0,&dwThreadID); 預設 Stack Size 回傳的 thread ID 傳給 thread fun 的參數 Primary thread CreateThread(…) DWORD WINAPI FirstThread(PVOID pvParam){ } 建立的 thread Thread function 繼續執行 執行的過程
馬上寫個範例 DWORD WINAPI FirstThread(PVOID pvParam){ int* Data=(int*) pvParam; int sum=0; for(int i=0;i<=*Data;i++) sum+=i; Data[1]=sum; return(0); } Local 變數配置在thread 自己的 stack 中 int APIENTRY WinWain( … ){ int Data[2]={10,0}; DWORD dwThreadID; HANDLE hThread=CreateThread(NULL,0,FirstThread,(PVOID)Data,0,&dwThreadID); WaitForSingleObject(hThread,INFINITE); TCHAR Message[100]; wsprintf(Message,_T("答案為 %d"),Data[1]); MessageBox(NULL,Message,_T("1+2+3+...+10答案"),MB_OK); CloseHandle(hThread); return 0; } 立即執行 等待 thread 結束 組合你的答案 Win32CreateThread
系統建立 thread 的流程 呼叫 Create Thread Thread 本身 reference 與 primary thread reference Step 1 系統建立 * Thread kernel object * Usage counter = 2 ExitThread() 執行你的 thread function Step 2 設定 kernel 初值 1. Suspend count =1 2. Exit code =STILL_ACTIVE 3. signaled = FALSE 先暫停 VOID BaseThreadStar(){ } 呼叫 Step 3 Step 5 由 process 的 virtural address 中 配置 Stack 空間給 thread 使用 • 設定 kernel Object 中 context • SP 與 IP + 一堆 CPU 暫存器 Step 4 儲存CPU的狀態 把 thread fun 位址 與 參數 push 到 thread 專屬 Stack中 目前執行的程式碼位置
Thread Kernel Object 結構圖 Thread Stack Thread Kernel Object 的內容 高位址 pvParam pfnStartAddr 參數 Thread function 起始位址 Context 指向 Stack top 的資料 SP IP 其他 CPU registers 指向要執行的 function 低位址 Usage count = 2 Suspend count =1 Exit code = STILL_ACTIVE Signated = FALSE Kernel32.dll VOID BaseThreadStart(){ … }
接下來看看BaseThreadStart 函式的工作情形 VOID BaseThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) { __try { ExitThread( (pfnStartAddr)(pvParam) ); } __except(UnhandledExceptionFilter(GetExceptionInformation( ))) { ExitProcess(GetExceptionCode( )); } // NOTE: We never get here. } 1 3 呼叫你的 thread function 2 當執行完 thread fun 直接呼叫 ExitThread thread function 參數位址 4 當發生 Exception 時, 會終結 Process Kernel32.dll
啟動 thread 是否設定 CREATE_SUSPEND Yes 等到 Resume 才啟動 No 現在這個 thread 可以被 schedule 到 CPU 上執行了 設定 Suspend count =0 現在這個 thread 可以執行程式碼 以及處理資料了 將 context 中的暫存器 值載入CPU 執行 BaseThreadStart function BaseThreadStart 內部呼叫 ExitThread 結束 thread生命 ExitThread 結束
我們都知道 main 是由 primary thread 所執行. 那麼 primary thread是怎麼被建立的呢 ? 注意: 剛剛是 thread, 現在是 Process VOID BaseProcessStart(PPROCESS_START_ROUTINE pfnStartAddr) { __try { ExitThread((pfnStartAddr)( )); } __except(UnhandledExceptionFilter(GetExceptionInformation( ))) { ExitProcess(GetExceptionCode( )); } } C/C++ Startup Code
Terminating a Thread終結thread 的方法 • The thread function returns • ExitThread() • C++ 物件的解構子,不會被呼叫 • DLL 會被通知 • TerminateThread(HANDLE, DWORD) • 不一定會呼叫完,指定的 thread 就會被刪除 • 載入的 DLL 不會接到通知 • 包含 thread 的 process 被 terminate 請使用這個 不要使用 不要使用 非同步呼叫(asynchronous ): 不保證當 function returned 時,會執行完成工作 不要使用
When a Process terminate ExitProcess 與 ExitThread 的比較 • If you call ExitProcess and TerminateProcess terminate all threads in the process • ExitThread & TerminateThread only terminate a thread 在 Thread 中的 C++ 物件的 解構子不會被呼叫 修改
當 thread 被終結時,系統的行為是甚麼? • All User object handle owned by the thread are free 清除所有的 Window 與 Hook (攔截) Thread’s exit code: STILL_ACTIVE 傳進 ExitThread(參數) The kernel object signaled 若這是最後一個 Thread , 則刪除 process -1 Thread kernel Object usage count 減一
我要如何知道 thread 的狀態呢? 呼叫 GetExitCodeThread function 函式原型 BOOL GetExitCodeThread(HANDLE hThread, PDWORD pdwExitCode); 使用範例 HANDLE hThread=CreateThread(NULL,0,FirstThread,(PVOID)Data,0, &dwThreadID); WaitForSingleObject(hThread,INFINITE); DWORD dwExitCode; GetExitCodeThread(hThread,&dwExitCode); CloseHandle(hThread); 得到 Thread exit code
如果你有用到 C/C++ function,請使用 _beginthreadex 代替 uintptr_t _beginthreadex( void *security, unsigned stack_size, unsigned ( __stdcall *start_address )( void * ), void *arglist, unsigned initflag, unsigned *thrdaddr ); C/C++ 版本 HANDLECreateThread(LPSECURITY_ATTRIBUTESlpThreadAttributes, SIZE_TdwStackSize, LPTHREAD_START_ROUTINElpStartAddress, LPVOIDlpParameter, DWORDdwCreationFlags, LPDWORDlpThreadId ); 對照一下 Win32 API 版本 Microsoft’s C/C++ 程式庫開發小組認為 c程式碼不應該 depend on Windows 你發覺了嗎? 參數數目一樣,但是資料型態不同!
請使用 _beginthreadex • 直接替換會有問題! hThread=CreateThread(NULL,0,FirstThread,(PVOID)Data,0,&dwThreadID); hThread=_beginthreadex(NULL,0,FirstThread,(PVOID)Data,0,&dwThreadID); 你應該要轉型才對!
typedef unsigned (__stdcall *PTHREAD_START) (void *); #define chBEGINTHREADEX (psa,cbStack,pfnStartAddr, pvParam,fdwCreate, pdwThreadID) \ ((HANDLE) _beginthreadex( \ (void*) (psa), \ (unsigned)(cbStack),\ (PTHREAD_START) (pfnStartAddr),\ (void *) (pvParam),\ (unsigned)(fdwCreate),\ (unsigned*) (pdwThreadID))) 請使用 _beginthreadex 別忘了加上 process.h • 簡單的巨集 • #include <process.h> 換行一定要加 \ 定義巨集 hThread=chBEGINTHREADEX(NULL,0,FirstThread,(PVOID)Data,0,&dwThreadID); 用巨集幫我轉型 _beginthreadex 簡單範例
Global Variable & Local Variable 1 號 thread 2 號 thread int golbal=1; void Fun( void* p){ int x=123; } 存取自己的 Stack 中的 local variable Golbal=1 存取自己的 Stack 中的 local variable x=123 … x=123
Global Variable & Local Variable驗證程式 #include <windows.h> #include <process.h> #include <stdio.h> int main(){ _beginthread(fun,0,"一號"); _beginthread(fun,0,"二號"); getchar(); return 0; } int global; void fun(void *p){ char *name=(char*) p; int x=123; for(int i=0;i<5;i++){ printf("===== %s ======\n",name); printf("x 變數的位址 ==> %p \n",&x); printf("全區域變數的位址 ==> %p \n\n",&global); Sleep(1000); } } 關鍵段落: 請注意兩個 thread 列印出的位址 驗證 Local Variable
What’s Problem? error C2065: '_beginthreadex' : undeclared identifier • 因為 VC 預設的 C/C++ run-time library 為 single-threaded, 所以你必須選擇 multithread版本的 runtime library VC 6 的設定 Project->Settings…
Six C/C++ run-time libraries 預設值 singlethread multithread 主要是因為1970年時,C run-time library 開發人員沒有考慮到 multi-thread 的情況 為何要那麼多 libraries 選項? 為何不一次搞定?
處理 Global 變數 Standard C/C++ run-time library 並沒有支援 multithread • 請看下面程式碼, 當 system 呼叫與 if 之間,有 thread 切換, 則 global 變數 errno 則會發生問題 BOOL fFailure=(system(“NOTEPAD.exe README.txt”) == -1); if(fFailure){ switch(errno){ case E2BIG: break; … } } 因為 errno 是全區域變數 所以任何 thread 都可能會更改它 因此導致程式產生不確定的結果 C/C++ run-time library 程式片段
處理 Global 變數 Standard C/C++ run-time library 並沒有支援 multithread C/C++ run-time library 大量使用 global variable處理狀態紀錄 C/C++ 的 Global variable 諸如 errno,_doserrno, strtok,_wcstor , strerror, tmpnam, tmpfile, asctime, … BOOL fFailure=(system(“NOTEPAD.exe README.txt”) == -1); if(fFailure){ switch( errno ){ case E2BIG: break; … } } 對於這種全區域變數的存取 必須做轉換 error 轉換成 function call #define errno (*_errno( ))
程式設計技巧經全區域變數的存取轉換成內部 變數存取 error 符號定義為成 function call #define errno (*_errno( )) Thread 內部使用 的區域 int Myerrno=123; int* _errno(){ return &Myerrno; } void main(){ * _errno()=555; } 傳回 Myerrno 的位址 設定 errno 值 讀取 errno 值 errno= 555; 轉換為設定thread 內部變數 Myerrno=555; int x = errno; 轉換成 int x= *_errno();
接下來, 就要來看看 C/C++ run-time library 的詳細實作情形了 C/C++ run-time library 設定 • 在 multi-thread 環境下, run-time library 的全區域變數存取,需要轉換. 所以你必須 link multi-thread 專用的 library.
來看看為甚麼 _beginthreadex 可以全區域變數的解決問題 ? • C/C++ run-time source code • Microsoft Visual Studio .NET 2003\Vc7\crt\src • threadex.c • 有關係的幾個 C/C++ 處理 thread 的function • _beginthreadex(): 你建立新thread必須呼叫 • _threadstartex(): 由 _beginthreadex 所呼叫 • _endthreadex : 當結束 thread 時,自動呼叫用來解除暫存空間使用 1 2 3
三個functions之間的關係 Step 1 Step 2 _beginthreadex(指定 callback function) _beginstartex(指定 你的 thread function位址) 配置 Thread 專用記憶體 來存放 c/c++ run-time 需要的全區域變數 呼叫你的 thread function CreateThread(…_threadstartex,…) Step 3 繼續執行 呼叫 _endthread()釋放 記憶體 幫你把 CreateThread 包起來了
配置 Thread 專用記憶體 來存放 c/c++ run-time 會存取的 全區域變數 接下來, 看看 _threadstartex 怎麼做的? 發生問題時的處理程式碼 _beginthreadex 虛擬碼 主要的工作: 就是 配置 專用空間 與 建立 thread unsigned long __cdecl _beginthreadex( … ){ _ptiddata ptd; // thread’s data block 指標 // 配置 data block 給新的 thread if ((ptd = _calloc_crt(1, sizeof(struct tiddata))) = = NULL) goto error_return; … // 將 callback function 的位址與相關參數 ptd 中 ptd->_initaddr = (void *) pfnStartAddr; ptd->_initarg = pvParam; // 建立新的 Thread thdl = (unsigned long) CreateThread(psa, cbStack, _threadstartex, (PVOID) ptd, fdwCreate, pdwThreadID); return (thdl); error_return: _free_ctr(ptd); return ((unsigned long ) 0); } 1 呼叫_threadstartex (看下一頁) 2
呼叫使用者指定的 thread function 指定參數 _threadstartex 虛擬碼 主要的工作: 就是 分配 專用空間 與 呼叫thread function static unsigned long WINAPI threadstartex (void* ptd){ // 將 tiddata block 指定給目前新建立的 thread TlsSetValue(__tlsindex, ptd); // 將目前 thread id 存起來 ((_ptiddata) ptd)->_tid = GetCurrentThreadId( ); … _try{ _endthreadex(( (unsigned (WINAPI *)(void *)) (((_ptiddata)ptd)->_initaddr) ) ( ((_ptiddata)ptd)->_initarg ) ) ; }__except( … ){ _exit(GetExceptionCode()); } return(0L); } 1 將配置好的空間, associate 給目前 的 thread 這樣, 每個 thread 就有自己的空間 存放 global variable 了 2 最後呼叫 _endthreadex,釋放記憶體 Structure Exception Handle 是用來防止你的程式發生 exception 或 用來處理 C 的 signal function 用的
_endthread 虛擬碼 主要的工作: 就是free專用空間 void __cdecl _endthreadex (unsigned retcode) { _ptiddata ptd; // 取得 tiddata 位址 ptd = _getptd( ); // Free the tiddata block. _freeptd(ptd); // Terminate the thread. ExitThread(retcode); } 當 thread 結束時, 釋放存放 c/c++ run-time Library變數的空間
於是當你執行 int d=errorno; 時 • 經由下面的巨集轉換為 function call的return 如果你定義 multithread #if defined(_MT) || defined(_DLL) extern int * __cdecl _errno(void); #define errno (*_errno( )) #else /* ndef _MT && ndef _DLL */ extern int errno; #endif /* _MT || _DLL */ 定義處理 errorno 的 function Errno 呼叫 _errno 函式,並且得 到位址的內容
_beginthread 範例 #include <windows.h> #include <process.h> /* _beginthread, _endthread */ #include <conio.h> //_getch #include <iostream> using namespace std; void loop_fun(void* ch); // 印出指定的字元 void CheckKey( void *dummy ); // 檢查否要離開程式 BOOL repeat = TRUE; int main() { char data='_'; _beginthread( loop_fun, 0,(void*)data); // ch 指標變數因為是 void* 型態,所以要先cast _beginthread( loop_fun, 0,(void*)'*'); // 建立一個 thread, 隨時檢查使用者是否要終止程式 (藉由 repeat 控制) _beginthread( CheckKey, 0, NULL ); // 等待結束 while( repeat ){ } } 1 建立第一條 thread 2 建立第二條 thread 建立第三條 thread 當 repeat =0 時, primary thread 就會 terminate
// 印出指定的字元 void loop_fun(void *ch){ char data=(char)ch; while(repeat){ cout << data << " "; Sleep( 100L ); } } // 隨時檢查使用者是否要終止程式 void CheckKey( void *dummy ){ _getch(); // 等待使用者鍵入任意字元 (不會秀出字來) repeat = 0; } 讓 primary thread 的無限迴圈跳出來
Remark 問題一: 是否要明確呼叫 _endthread() ? 你指的是 標題為: "_threadstartex 虛擬碼" 的投影片嗎? 這頁投影片是把C/C++ run-time 的原始程式秀出來, 也就是那個 _endthreadex 所以你才不用 呼叫. 嗯.. 這個我在上課時,就懷疑應該是不用呼叫 _endthread() 但是你也可以明確的呼叫, 不過這只是確定一下而已. MSDN MSDN 原文說明 The _endthread function terminates a thread created by _beginthread. Threads terminate automatically when they finish. The _endthread function is useful for conditional termination from within a thread.
Remark 問題二: thread kernel object usage counter 的問題 <sol> 當你建立 thread 時, 還記得 thread 的 kernel object 的 usage count 會增加 2 嗎? 一個是自己, 另一個就是 primary thread, 所以當你在 primary thread 明確的呼叫CloseHandle( (HANDLE) x), 這會把 x 這個 thread 的 kernerl object usage count -1 (這時 x thread kernel object usage count 由 2 變成 1), 當 x 的 thread function 結束時, 系統會自動加上 _endthread(). 並且自動 CloseHandle(). 此時, thread 的 kernel object usage count 由 1 變成 0, 系統把這個 thread object 回收.
Remark • --注意事項: _beginthread() 與 _beginthreadex() 之間的差別 • _beginthread() 對應 _endthread() • _beginthreadex() 對應 _endthreadex() 因為你使用 _beginthread() 建立 thread, 所以 Win32 會在呼叫 _endthread() 結束前,自動 Close 該 thread 的 Handle • 若你用 _beginthreadex() 建立 thread, 則系統不會幫你呼叫 CloseHandle) • _beginthread 建立的 thread, 當 thread function 結束時 Reference: 1. ms-help://MS.MSDNQTR.2004JAN.1033/vclib/html/_crt__beginthread.2c_._beginthreadex.htm 2. http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vccore98/html/_crt__beginthread.2c_._beginthreadex.asp