windows平台的多線程同步互斥:從核心源碼分析—-小話多線程(3)

來源:互聯網
上載者:User

作者:陳曦

日期: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]  

轉載請註明出處

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.