windows平台的多线程同步互斥:从内核源码分析----小话多线程(3)

作者:陈曦

日期: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内核源代码)。

NTSTATUS
NtCreateEvent (
    __out PHANDLE EventHandle,
    __in ACCESS_MASK DesiredAccess,
    __in_opt POBJECT_ATTRIBUTES ObjectAttributes,
    __in EVENT_TYPE EventType,
    __in BOOLEAN InitialState
    )

首先我们看到上面的声明是CreateEvent的内核实现函数声明。具体实现如下:
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;
}

我们能够发现,它主要调用了3个函数:ObCreateObject, KeInitializeEvent和ObInsertObject.

第一个主要是创建一个对象,第二个为内核初始化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;
}

对于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函数:

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;
}

我想我不用解释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]  

转载请注明出处


  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值