转载请注明出处
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修改的事件,然后就等待子线程发送结束事件,然后结束。
这样的话,主线程和子线程可以按照预定的步骤执行,而不会出现执行顺序出错的问题,运行结果:
Q4: 形如上面的例子CreateEvent创建的event在内核中到底是什么?
A: 为了更清楚地弄清楚它到底是什么,我们先查看内核源代码(wrkv1.2, nt内核源代码,windows xp, windows server 2003内核源代码)。
NTSTATUS NtCreateEvent ( __out PHANDLE EventHandle, __in ACCESS_MASK DesiredAccess, __in_opt POBJECT_ATTRIBUTES ObjectAttributes, __in EVENT_TYPE EventType, __in BOOLEAN InitialState )
NTSTATUS NtCreateEvent ( __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; }
第一个主要是创建一个对象,第二个为内核初始化event对象,第三个是将此对象插入进程句柄表中。
对于KeInitializeEvent函数的实现,如下:
VOID KeInitializeEvent ( __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; }
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函数:
FORCEINLINE VOID KiWaitTestSynchronizationObject ( 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函数:
VOID FASTCALL KiUnwaitThread ( 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; }
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]
转载请注明出处