作者:陳曦
日期:2012-8-16 13:05:34
環境:[win7 32位作業系統 Intel i3 支援64位指令 VS2010; wrk-v1.2 ; Source Insight]
轉載請註明出處
Q1: 舉個windows平台簡單的線程例子吧。
A: 如下,儲存為thread_test.c:
#include <windows.h>#include <stdio.h> #define PRINT_U(ulongValue) printf(#ulongValue" is %lu\n", ((unsigned long)ulongValue));int main() { HANDLE thread = GetCurrentThread(); HANDLE process = GetCurrentProcess(); PRINT_U(GetThreadId(thread)) PRINT_U(GetProcessId(process)) getchar(); return 0; }
編譯成thread_test.exe, 運行:
可以看到,獲得了此線程的ID和所屬進程的ID; 我們同時可以從工作管理員中查看:
這裡也可以看到,PID確實是輸出的那樣,線程數為1,這表明只有一個主線程。如果希望查看更多的資訊,可以使用微軟提供的procexp.exe(sysinternals提供)查看:
可以看到上面thread_text.exe進程所屬的位置。雙擊進入:
這裡可以看到,此線程的ID,確實是上面輸出的7416; 同時也可以看到,此線程是從_mainCRTStartup啟動並執行。點擊stack按鈕查看具體堆棧資訊:
上面的圖示具體描述了此線程啟動並執行堆棧資訊,同時也可以看到線程運行在不同模組的位置(注意: ntkr128g.exe是本機因為要識別4G記憶體新安裝的核心,正常情況下是ntoskrnl.exe或者ntkrnlpa.exe. ). 這裡也可以看到線程運行於核心狀態調用的關係。
Q2: CreateThread和_beginthread到底有什麼區別?為什麼人們老說使用CreateThread可能導致記憶體泄露?
A: 從目的的角度來說,它們都是為了建立一個線程;但是具體到細節,它們又有不同:前者是系統API,這意味著它沒有和通常程式會使用的C庫等庫綁定,後者是微軟提供的c運行時函數。所以,_beginthread可能會做一些維持c庫正常啟動並執行事情,而CreateThread函數就很單純。查看它們的原始碼會很容易找到它們的區別,這裡就不貼代碼了。
如果已經知道它們所屬的層次不同,就很容易理解為什麼CreateThread建立線程可能會導致記憶體泄露了。
不過在win7或者2003 server等平台上,即使使用CreateThread建立子線程, 子線程中調用c庫函數strtok, 依然不會發生泄露,原因在於線程退出釋放Fiber Local Storage從而正確地釋放了線程局部儲存的資料。
如下代碼:
#include <windows.h>#include <stdio.h> #include <process.h>#define PRINT_U(ulongValue) printf(#ulongValue" is %lu\n", ((unsigned long)ulongValue));DWORD thread_func(LPVOID *arg){ char str[] = "111,222,333"; char *p; printf("thread_func begins...\n"); // print all tokens seperated by ',' p = strtok(str, ","); while(p) {printf("token:%s \n", p);p = strtok(NULL, ","); } printf("thread_func ends...\n"); return 0;}int main() { HANDLE thread; thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)thread_func, NULL, 0, NULL); if(!thread) { perror("CreateThread error"); return -1; } WaitForSingleObject(thread, INFINITE); // wait for son thread to exit CloseHandle(thread); printf("main thread will end...\n"); return 0; }
在tidtable.c檔案中的_freefls函數開始加斷點,調試運行:
可以看到,子線程退出線程過程中執行了_freefls函數,它的內部將釋放TLS結構ptd.
當然,依然要注意:此程式連結C庫的方式是靜態連結,即採用/MT或者/MTd方式,而不是採用動態連結DLL的方式/MD或者/MDd的方式。因為採用動態連結C庫的方式DLL初始化和退出時會自動釋放TLS資料,而無法驗證ExitThread是否釋放TLS.
另外,正如上面之前提到的,我在win7以及windows server 2003的虛擬機器上面運行程式,都符合上面的分析,即CreateThread建立線程後線程內部調用使用TLS結構的函數,比如strtok後,並不會造成記憶體泄露;但是,我在XP上運行此程式,就發現了記憶體泄露。具體就不貼圖了,大家可以自行測試(最好使用while迴圈不斷建立線程這樣很明顯觀察到記憶體泄露的過程,在win7或者windows server 2003上,記憶體會上下浮動,但是隨著線程結束釋放了對應的結構,進程佔用的記憶體始終保持在一個小波動的範圍,而在xp上明顯能看到記憶體使用量迅速增加)。
不過不管一個進程泄露了多少記憶體,最終進程結束的時候都會釋放這些記憶體,所以當結束後,這些記憶體被回收了,不用害怕你的機器運行了幾次記憶體沒了。
另外,我查了一下ntdll.dll模組中_RtlProcessFlsData函數的出處,發現它是從vista系統開始引入的,所以我猜測vista系統和上面的win7, server 2003運行情況類似,這個沒有測試,如果誰正好有這個系統或虛擬機器,方便測試,可以幫忙測試一下。
Q3: CreateEvent建立的事件對象和CreateMutex建立的互斥體到底有什麼區別?
A: 其實event直觀的感覺更傾向於同步,而mutex更傾向於互斥;但是,同步互斥本來就不是矛盾體,同步有時就意味著互斥,互斥也就意味著需要同步,很多時候它們是結合在一起使用的。對於mutex不再舉例,下面對於event舉個例子,儲存為test_event.c:
#include <windows.h>#include <stdio.h> #include <process.h>#include <tchar.h>#define PRINT_U(ulongValue) printf(#ulongValue" is %lu\n", ((unsigned long)ulongValue));HANDLE waitDataEvent;HANDLE waitThreadEndEvent;static int data = 0;DWORD thread_func(LPVOID *arg){ printf("thread_func begins...\n"); WaitForSingleObject(waitDataEvent, INFINITE); // wait for the dataEvent's be signaled Sleep(1000); printf("son thread update:main thread has set data:%d...\n", data); printf("thread_func ends...\n"); SetEvent(waitThreadEndEvent); // tell main thread that it will exit return 0;}int main() { HANDLE thread; thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)thread_func, NULL, 0, NULL); if(!thread) {perror("CreateThread error");return -1; } waitDataEvent = CreateEvent(NULL, TRUE, FALSE, _T("dataEvent")); // init to FALSE if(!waitDataEvent) {perror("CreateEvent waitDataEvent error");CloseHandle(thread);return -1; } waitThreadEndEvent = CreateEvent(NULL, TRUE, FALSE, _T("threadEvent")); // init to FALSE if(!waitThreadEndEvent) {perror("CreateEvent waitThreadEndEvent error");CloseHandle(thread);CloseHandle(waitDataEvent);return -1; } Sleep(2000); // set data and let son thread go on... data = 1; SetEvent(waitDataEvent); // wait the son thread end WaitForSingleObject(waitThreadEndEvent, INFINITE); Sleep(1000); CloseHandle(thread); CloseHandle(waitDataEvent); CloseHandle(waitThreadEndEvent); printf("main thread will end...\n"); return 0; }
上面可以清晰地看到,子線程首先等待主線程修改data的數值,然後輸出它,之後準備結束,通知主線程它將要結束;
主線程修改data後發送data修改的事件,然後就等待子線程發送結束事件,然後結束。
這樣的話,主線程和子線程可以按照預定的步驟執行,而不會出現執行順序出錯的問題,運行結果:
Q4: 形如上面的例子CreateEvent建立的event在核心中到底是什嗎?
A: 為了更清楚地弄清楚它到底是什麼,我們先查看核心原始碼(wrkv1.2, nt核心原始碼,windows xp, windows server 2003核心原始碼)。
NTSTATUSNtCreateEvent ( __out PHANDLE EventHandle, __in ACCESS_MASK DesiredAccess, __in_opt POBJECT_ATTRIBUTES ObjectAttributes, __in EVENT_TYPE EventType, __in BOOLEAN InitialState )
首先我們看到上面的聲明是CreateEvent的核心實現函式宣告。具體實現如下:
NTSTATUSNtCreateEvent ( __out PHANDLE EventHandle, __in ACCESS_MASK DesiredAccess, __in_opt POBJECT_ATTRIBUTES ObjectAttributes, __in EVENT_TYPE EventType, __in BOOLEAN InitialState )/*++Routine Description: This function creates an event object, sets it initial state to the specified value, and opens a handle to the object with the specified desired access.Arguments: EventHandle - Supplies a pointer to a variable that will receive the event object handle. DesiredAccess - Supplies the desired types of access for the event object. ObjectAttributes - Supplies a pointer to an object attributes structure. EventType - Supplies the type of the event (autoclearing or notification). InitialState - Supplies the initial state of the event object.Return Value: NTSTATUS.--*/{ PVOID Event; HANDLE Handle; KPROCESSOR_MODE PreviousMode; NTSTATUS Status; // // Get previous processor mode and probe output handle address if // necessary. // PreviousMode = KeGetPreviousMode(); if (PreviousMode != KernelMode) { try { ProbeForWriteHandle(EventHandle); } except(EXCEPTION_EXECUTE_HANDLER) { return GetExceptionCode(); } } // // Check argument validity. // if ((EventType != NotificationEvent) && (EventType != SynchronizationEvent)) { return STATUS_INVALID_PARAMETER; } // // Allocate event object. // Status = ObCreateObject(PreviousMode, ExEventObjectType, ObjectAttributes, PreviousMode, NULL, sizeof(KEVENT), 0, 0, &Event); // // If the event object was successfully allocated, then initialize the // event object and attempt to insert the event object in the current // process' handle table. // if (NT_SUCCESS(Status)) { KeInitializeEvent((PKEVENT)Event, EventType, InitialState); Status = ObInsertObject(Event, NULL, DesiredAccess, 0, NULL, &Handle); // // If the event object was successfully inserted in the current // process' handle table, then attempt to write the event object // handle value. If the write attempt fails, then do not report // an error. When the caller attempts to access the handle value, // an access violation will occur. // if (NT_SUCCESS(Status)) { if (PreviousMode != KernelMode) { try { *EventHandle = Handle; } except(EXCEPTION_EXECUTE_HANDLER) { NOTHING; } } else { *EventHandle = Handle; } } } // // Return service status. // return Status;}
我們能夠發現,它主要調用了3個函數:ObCreateObject, KeInitializeEvent和ObInsertObject.
第一個主要是建立一個對象,第二個為核心初始化event對象,第三個是將此對象插入進程控制代碼表中。
對於KeInitializeEvent函數的實現,如下:
VOIDKeInitializeEvent ( __out PRKEVENT Event, __in EVENT_TYPE Type, __in BOOLEAN State )/*++Routine Description: This function initializes a kernel event object. The initial signal state of the object is set to the specified value.Arguments: Event - Supplies a pointer to a dispatcher object of type event. Type - Supplies the type of event; NotificationEvent or SynchronizationEvent. State - Supplies the initial signal state of the event object.Return Value: None.--*/{ // // Initialize standard dispatcher object header, set initial signal // state of event object, and set the type of event object. // Event->Header.Type = (UCHAR)Type; Event->Header.Size = sizeof(KEVENT) / sizeof(LONG); Event->Header.SignalState = State; InitializeListHead(&Event->Header.WaitListHead); return;}
對於event結構:
typedef struct _KEVENT { DISPATCHER_HEADER Header;} KEVENT, *PKEVENT, *PRKEVENT;
typedef struct _DISPATCHER_HEADER { union { struct { UCHAR Type; // obj type union { UCHAR Absolute; UCHAR NpxIrql; }; union { UCHAR Size; // obj size,unit as sizeof(DWORD) UCHAR Hand; }; union { UCHAR Inserted; BOOLEAN DebugActive; }; }; volatile LONG Lock; }; LONG SignalState; LIST_ENTRY WaitListHead; // the objs that wait for this obj } DISPATCHER_HEADER;
從上面,我們可以看出,對於event,核心其實儲存了一個資料對象,並記錄了它的基本狀態和等待列表。
可是核心調度線程是如何決定哪個線程該掛起,哪個可以就緒或者運行,保證線程同步互斥的正確的呢?
正如下面NtSetEvent代碼內部做的那樣,它內部會調用KiWaitTestSynchronizationObject函數:
FORCEINLINEVOIDKiWaitTestSynchronizationObject ( IN PVOID Object, IN KPRIORITY Increment )/*++Routine Description: This function tests if a wait can be satisfied when a synchronization dispatcher object attains a state of signaled. Synchronization objects include synchronization events and synchronization timers.Arguments: Object - Supplies a pointer to an event object. Increment - Supplies the priority increment.Return Value: None.--*/{ PKEVENT Event = Object; PLIST_ENTRY ListHead; PRKTHREAD Thread; PRKWAIT_BLOCK WaitBlock; PLIST_ENTRY WaitEntry; // // As long as the signal state of the specified event is signaled and // there are waiters in the event wait list, then try to satisfy a wait. // ListHead = &Event->Header.WaitListHead; ASSERT(IsListEmpty(&Event->Header.WaitListHead) == FALSE); WaitEntry = ListHead->Flink; do { // // Get the address of the wait block and the thread doing the wait. // WaitBlock = CONTAINING_RECORD(WaitEntry, KWAIT_BLOCK, WaitListEntry); Thread = WaitBlock->Thread; // // If the wait type is wait any, then satisfy the wait, unwait the // thread with the wait key status, and exit loop. Otherwise, unwait // the thread with a kernel APC status and continue the loop. // if (WaitBlock->WaitType == WaitAny) { Event->Header.SignalState = 0; KiUnwaitThread(Thread, (NTSTATUS)WaitBlock->WaitKey, Increment); break; } KiUnwaitThread(Thread, STATUS_KERNEL_APC, Increment); WaitEntry = ListHead->Flink; } while (WaitEntry != ListHead); return;}
它將對此event的等待線程列表挨個發送啟用訊號, 當然最後線程會不會可以繼續執行那依賴於它們具體設定的狀態。它內部的核心代碼為KiUnwaitThread函數:
VOIDFASTCALLKiUnwaitThread ( IN PRKTHREAD Thread, IN LONG_PTR WaitStatus, IN KPRIORITY Increment )/*++Routine Description: This function unwaits a thread, sets the thread's wait completion status, calculates the thread's new priority, and either readies the thread for execution or adds the thread to a list of threads to be readied later.Arguments: Thread - Supplies a pointer to a dispatcher object of type thread. WaitStatus - Supplies the wait completion status. Increment - Supplies the priority increment that is to be applied to the thread's priority.Return Value: None.--*/{ // // Unlink thread from the appropriate wait queues and set the wait // completion status. // KiUnlinkThread(Thread, WaitStatus); // // Set unwait priority adjustment parameters. // ASSERT(Increment >= 0); Thread->AdjustIncrement = (SCHAR)Increment; Thread->AdjustReason = (UCHAR)AdjustUnwait; // // Ready the thread for execution. // KiReadyThread(Thread); return;}
我想我不用解釋KiReadyThread的意義了。當然,對於其它同步互斥對象,比如mutex, 實現互斥的過程也是類似的,這裡不一一列舉了。
Q5: windows上可以使用pthread函數庫嗎?
A: 微軟官方貌似沒有發布pthread庫,但是有開原始碼,詳情請進:http://sources.redhat.com/pthreads-win32/
作者:陳曦
日期:2012-8-16 13:05:34
環境:[win7 32位作業系統 Intel i3 支援64位指令 VS2010; wrk-v1.2 ; Source Insight]
轉載請註明出處