1. 程式人生 > >Win32線程安全問題.同步函數

Win32線程安全問題.同步函數

賦值 zh-cn The 可能 init pre 一份 互斥 using

              線程安全問題.同步函數

一丶簡介什麽是線程安全

  通過上面幾講.我們知道了線程怎麽創建.線程切換的原理(CONTEXT結構) 每個線程在切換的時候都有自己的堆棧.

但是這樣會有安全問題. 為什麽? 我們每個線程都使用自己的局部變量這個是沒有安全問題的. 但是線程可能會使用全局變量.這樣很有可能會產生安全問題.為什麽是很有可能.

1.有全局變量的情況下.有可能會有安全問題.

2.對全局變量進行寫操作.則一定有安全問題.

上面兩個條件都具備,線程才是不安全的.

為什麽是不安全的.

試想一下. 如果這個全局變量在更改.另一個線程也更改了.最後則會出現兩個線程同時更改這個全局變量. 問題就會出現在這.

例如以下代碼:

// 臨界區同步函數.cpp : 定義控制臺應用程序的入口點。
//

#include "stdafx.h"
#include <Windows.h>
DWORD g_Number = 10;
DWORD WINAPI MyThreadFun1(LPVOID lParame)
{
    while (g_Number > 0)
    {
        printf("+++剩下Number個數 = %d\r\n", g_Number);
        g_Number--;
        printf("+++當前的Number個數 = %d\r\n"
, g_Number); } return 0; } DWORD WINAPI MyThreadFun2(LPVOID lParame) { while (g_Number > 0 ) { printf("***剩下Number個數 = %d\r\n", g_Number); g_Number--; //產生線程安全問題 printf("***當前的Number個數 = %d\r\n", g_Number); } return
0; } int main(int argc,char *argv[]) { HANDLE hThreadHand[2] = { NULL }; hThreadHand[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThreadFun1, NULL, 0, NULL); hThreadHand[1] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThreadFun2, NULL, 0, NULL); //創建兩個線程 WaitForMultipleObjects(2, hThreadHand, TRUE, INFINITE); printf("Number個數 = %d \r\n", g_Number); system("pause"); return 0; }

上面的代碼很簡單. 看下運行結果

技術分享圖片

為什麽會產生這個問題.原因是.在線程中我們有個地方

while(全局變量 > 0) 則會執行下邊代碼. 但是很有可能執行完這一句. 線程發生了切換. 去執行另一個線程去了. 最終會產生這樣的結果.

如果看反匯編.則會發現 全局變量--的地方.匯編代碼 並不是一局. 如果發生線程切換則會出現錯誤.

技術分享圖片

首先獲取全局變量的值.

然後sub -1

最後重新賦值.

很有可能在sun eax 1的時候就發生了切換. 這樣就有安全問題了.為了解決這些問題.我們必須想辦法. 所以Windows提供了一組線程同步的函數.

二丶線程同步函數之臨界區

什麽時候臨界區. 臨界區的意思就是 這一個區域我給你鎖定.當前有且只能有一個線程來執行我們臨界區的代碼.

而臨界資源是一個全局變量

臨界區的使用步驟.

1.創建全局原子變量.

2.初始化全原子變量

3.進入臨界區

4.釋放臨界區.

5.刪除臨界區.

具體API:

  1.全局原子變量

CRITICAL_SECTION g_cs;  //直接創建即可.不用關心內部實現.

  2.初始化全局原子變量.InitializeCriticalSection

    _Maybe_raises_SEH_exception_ VOID InitializeCriticalSection(
        LPCRITICAL_SECTION lpCriticalSection                       //傳入全局原子變量的地址
    );

3.使用的API 進入臨界區.

void EnterCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection      //全局原子變量
);

下面還有一個. 是嘗試無阻塞模式進入臨界區. 意思就是內部加了一個判斷.是否死鎖了.

BOOL TryEnterCriticalSection(                  返回吃持有的臨界區對象.如果成功的情況下.
  LPCRITICAL_SECTION lpCriticalSection
);

  4.使用API 釋放臨界區.

  

void LeaveCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection         //全局原子對象
);

  5.刪除臨界區對象.

void DeleteCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
);

代碼例子:

// 臨界區同步函數.cpp : 定義控制臺應用程序的入口點。
//

#include "stdafx.h"
#include <Windows.h>
//創建臨界區結構
CRITICAL_SECTION g_cs;

DWORD g_Number = 10;
DWORD WINAPI MyThreadFun1(LPVOID lParame)
{
    EnterCriticalSection(&g_cs); //進入臨界區
    while (g_Number > 0)
    {
        printf("+++剩下Number個數 = %d\r\n", g_Number);
        g_Number--;
        printf("+++當前的Number個數 = %d\r\n", g_Number);
    }
    LeaveCriticalSection(&g_cs);
    return 0;
}

DWORD WINAPI MyThreadFun2(LPVOID lParame)
{
    EnterCriticalSection(&g_cs); //進入臨界區
    while (g_Number > 0 )
    {
        printf("***剩下Number個數 = %d\r\n", g_Number);
        g_Number--;                                                                 //while語句內就是臨界區了.有且只能一個線程訪問.
        printf("***當前的Number個數 = %d\r\n", g_Number);
    }
    LeaveCriticalSection(&g_cs);
    return 0;
}

int main(int argc,char *argv[])
{
    //初始化臨界區全局原子變量
    InitializeCriticalSectionAndSpinCount(&g_cs, 0x00000400);
    //InitializeCriticalSection(&g_cs);                      //初始化臨界區.兩個API都可以.

    HANDLE hThreadHand[2] = { NULL };
    hThreadHand[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThreadFun1, NULL, 0, NULL);
    hThreadHand[1] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThreadFun2, NULL, 0, NULL); //創建兩個線程
    WaitForMultipleObjects(2, hThreadHand, TRUE, INFINITE);
    
    DeleteCriticalSection(&g_cs); //刪除臨界區.

    printf("+Number個數 = %d \r\n", g_Number);
    system("pause");
    return 0;
}

官方MSDN例子:

鏈接: https://docs.microsoft.com/zh-cn/windows/desktop/Sync/using-critical-section-objects

三丶線程同步之互斥體

1.臨界區缺點.以及衍生出來的跨進程保護

上面講了臨界區. 但是我們的臨界資源是一個全局變量.例如下圖:

技術分享圖片

如果我們的臨界資源是一個文件. 需要兩個進程都要訪問怎麽辦? 此時臨界區已經不可以跨進程使用了.

2.跨進程控制.

  跨進程控制就是指 不同進程中的多線程控制安全..比如A進程訪問臨界資源的時候. B進程不能訪問. 因為臨界區的 令牌.也就是我們說的全局原子變量.只能在應用層.

但是如果放到內核中就好辦了. 如下圖所示

  技術分享圖片

A進程的線程從內核中獲取互斥體. 為0 還是為1. B進程一樣. 如果為 0 則可以進行訪問臨界資源. 訪問的時候.互斥體則設置為1(也就是令牌設置為1)這樣B進程就獲取不到了.自然不能訪問

臨界區資源了.

3.互斥體操作API

  既然明白了互斥體是一個內核層的原子操作.那麽我們就可以使用API 進行操作了.

操作步驟.

    1.創建互斥體. 信號量設置為有信號的狀態 例如全局的原子變量現在是有信號.是可以進行訪問的.

    2.獲取信號狀態. 如果有信號則進入互斥體臨界區執行代碼.此時互斥體信號為無信號. 也就是說別的進程訪問的時候.因為沒有信號.執行不了代碼.

    3.釋放互斥體. 信號狀態為有信號. 此時別的進程信號已經有了.所以可以進行訪問了.

具體API:

1.創建互斥體

HANDLE CreateMutexA(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,          SD安全屬性.句柄是否可以繼承.每個內核對象API都擁有.
  BOOL                  bInitialOwner,              初始的信號量狀態. false為有信號. 獲取令牌的時候可以獲取到. True為無信號. 且如果為True互斥體對象為線程擁有者.
  LPCSTR                lpName                      全局名字. 根據名字尋找互斥體. 
);

2.獲取令牌.

  

DWORD WaitForSingleObject(
  HANDLE hHandle,                          等待的內核對象
  DWORD  dwMilliseconds                 等待的時間
);

調用此函數之後.信號為無信號.別的進程是進入不了互斥體臨界區的.

3.釋放互斥體

  

BOOL ReleaseMutex(
  HANDLE hMutex
);

調用完比之後.互斥體為有信號.可以使用了.

代碼例子:

  兩個工程代碼是一樣的.貼一份出來.

#include "stdafx.h"
#include <Windows.h>
//創建臨界區結構

int main(int argc,char *argv[])
{
    //初始化臨界區全局原子變量
    HANDLE MutexHandle = CreateMutex(NULL, FALSE, TEXT("AAA"));  //創建互斥體. 信號量為0. 有信號的狀態.wait可以等待

    WaitForSingleObject(MutexHandle,INFINITE);
    

    for (size_t i = 0; i < 10; i++)
    {
        Sleep(1000);
        printf("A進程訪問臨街資源中臨街資源ID = %d \r\n", i);
    }

    ReleaseMutex(MutexHandle);
    return 0;
}

先運行A進程在運行B進程. 則B進程處於卡死狀態.

技術分享圖片

實現了同步. 除非A進程釋放互斥體句柄使信號變為有信號.此時才可以訪問B

官方代碼例子:

  

#include <windows.h>
#include <stdio.h>

#define THREADCOUNT 2

HANDLE ghMutex; 

DWORD WINAPI WriteToDatabase( LPVOID );

int main( void )
{
    HANDLE aThread[THREADCOUNT];
    DWORD ThreadID;
    int i;

    // Create a mutex with no initial owner

    ghMutex = CreateMutex( 
        NULL,              // default security attributes
        FALSE,             // initially not owned               有信號
        NULL);             // unnamed mutex                     不需要跨進程使用.所以不用名字

    if (ghMutex == NULL) 
    {
        printf("CreateMutex error: %d\n", GetLastError());
        return 1;
    }

    // Create worker threads

    for( i=0; i < THREADCOUNT; i++ )
    {
        aThread[i] = CreateThread(                                       //創建  THREADCOUNT個線程
                     NULL,       // default security attributes
                     0,          // default stack size
                     (LPTHREAD_START_ROUTINE) WriteToDatabase, 
                     NULL,       // no thread function arguments
                     0,          // default creation flags
                     &ThreadID); // receive thread identifier

        if( aThread[i] == NULL )
        {
            printf("CreateThread error: %d\n", GetLastError());
            return 1;
        }
    }

    // Wait for all threads to terminate

    WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE);                   //等待線程執行完畢

    // Close thread and mutex handles

    for( i=0; i < THREADCOUNT; i++ )
        CloseHandle(aThread[i]);

    CloseHandle(ghMutex);

    return 0;
}

DWORD WINAPI WriteToDatabase( LPVOID lpParam )
{ 
    // lpParam not used in this example
    UNREFERENCED_PARAMETER(lpParam);

    DWORD dwCount=0, dwWaitResult; 

    // Request ownership of mutex.

    while( dwCount < 20 )
    { 
        dwWaitResult = WaitForSingleObject(                                //線程內部等待互斥體.因為一開始為FALSE所以有信號.第一次執行線程的時候則會執行. 
            ghMutex,    // handle to mutex
            INFINITE);  // no time-out interval
 
        switch (dwWaitResult) 
        {
            // The thread got ownership of the mutex
            case WAIT_OBJECT_0: 
                __try { 
                    // TODO: Write to the database
                    printf("Thread %d writing to database...\n", 
                            GetCurrentThreadId());
                    dwCount++;
                } 

                __finally { 
                    // Release ownership of the mutex object
                    if (! ReleaseMutex(ghMutex))                                         //執行完畢.釋放互斥體.信號量變為有信號. 其余線程等待的時候可以等到則可以繼續執行線程代碼
                    { 
                        // Handle error.
                    } 
                } 
                break; 

            // The thread got ownership of an abandoned mutex
            // The database is in an indeterminate state
            case WAIT_ABANDONED: 
                return FALSE; 
        }
    }
    return TRUE; 
}

Win32線程安全問題.同步函數