9.2 運用API實現執行緒同步

lyshark發表於2023-10-02

Windows 執行緒同步是指多個執行緒一同訪問共享資源時,為了避免資源的併發訪問導致資料的不一致或程式崩潰等問題,需要對執行緒的訪問進行協同和控制,以保證程式的正確性和穩定性。Windows提供了多種執行緒同步機制,以適應不同的併發程式設計場景。主要包括以下幾種:

  • 事件(Event):用於不同執行緒間的訊號通知。包括單次通知事件和重複通知事件兩種型別。
  • 互斥量(Mutex):用於控制對共享資源的訪問,具有獨佔性,可避免執行緒之間對共享資源的非法訪問。
  • 臨界區(CriticalSection):和互斥量類似,也用於控制對共享資源的訪問,但是是程式內部的,因此比較適用於同一程式中的執行緒同步控制。
  • 訊號量(Semaphore):用於基於計數器機制,控制併發資源的訪問數量。
  • 互鎖變數(Interlocked Variable):用於對變數的併發修改操作控制,可提供一定程度的原子性操作保證。

以上同步機制各有優缺點和適用場景,開發者應根據具體應用場景進行選擇和使用。線上程同步的實現過程中,需要注意競爭條件和死鎖的處理,以確保程式中的執行緒能協同工作,共享資源能夠正確訪問和修改。執行緒同步是併發程式設計中的重要基礎,對於開發高效、穩定的併發應用至關重要。

9.2.1 CreateEvent

CreateEvent 是Windows API提供的用於建立事件物件的函式之一,該函式用於建立一個事件物件,並返回一個表示該事件物件的控制程式碼。可以透過SetEvent函式將該事件物件設定為有訊號狀態,透過ResetEevent函式將該事件物件設定為無訊號狀態。當使用WaitForSingleObject或者WaitForMultipleObjects函式等待事件物件時,會阻塞執行緒直到事件狀態被置位。對於手動重置事件,需要呼叫ResetEvent函式手動將事件狀態置位。

CreateEvent 函式常用於執行緒同步和程式間通訊,在不同執行緒或者程式之間通知事件狀態的改變。例如,某個執行緒完成了一項任務,需要通知其它等待該任務完成的執行緒;或者某個程式需要和另一個程式進行協調,需要通知其它程式某個事件的發生等等。

CreateEvent 函式的函式原型如下:

HANDLE CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes,
  BOOL                  bManualReset,
  BOOL                  bInitialState,
  LPCTSTR               lpName
);

引數說明:

  • lpEventAttributes:指向SECURITY_ATTRIBUTES結構體的指標,指定事件物件的安全描述符和訪問許可權。通常設為NULL,表示使用預設值。
  • bManualReset:指定事件物件的型別,TRUE表示建立的是手動重置事件,FALSE表示建立的是自動重置事件。
  • bInitialState:指定事件物件的初始狀態,TRUE表示將事件物件設為有訊號狀態,FALSE表示將事件物件設為無訊號狀態。
  • lpName:指定事件物件的名稱,可以為NULL。

CreateEvent 是實現執行緒同步和程式通訊的重要手段之一,應用廣泛且易用。在第一章中我們建立的多執行緒環境可能會出現執行緒同步的問題,此時使用Event事件機制即可很好的解決,首先在初始化時透過CreateEvent將事件設定為False狀態,進入ThreadFunction執行緒時再次透過SetEvent釋放,以此即可實現執行緒同步順序執行的目的。

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

// 全域性資源
long g_nNum = 0;

// 子執行緒個數
const int THREAD_NUM = 10;

CRITICAL_SECTION  g_csThreadCode;
HANDLE g_hThreadEvent;

unsigned int __stdcall ThreadFunction(void *ptr)
{
  int nThreadNum = *(int *)ptr;

  // 執行緒函式中觸發事件
  SetEvent(g_hThreadEvent);

  // 進入執行緒鎖
  EnterCriticalSection(&g_csThreadCode);
  g_nNum++;
  printf("執行緒編號: %d --> 全域性資源值: %d --> 子執行緒ID: %d \n", nThreadNum, g_nNum, GetCurrentThreadId());

  // 離開執行緒鎖
  LeaveCriticalSection(&g_csThreadCode);
  return 0;
}

int main(int argc,char * argv[])
{
  unsigned int ThreadCount = 0;
  HANDLE  handle[THREAD_NUM];

  // 初始化自動將事件設定為False
  g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
  InitializeCriticalSection(&g_csThreadCode);

  for (int each = 0; each < THREAD_NUM; each++)
  {
    handle[each] = (HANDLE)_beginthreadex(NULL, 0, ThreadFunction, &each, 0, &ThreadCount);

    // 等待執行緒事件被觸發
    WaitForSingleObject(g_hThreadEvent, INFINITE);
  }

  WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);

  // 銷燬事件
  CloseHandle(g_hThreadEvent);
  DeleteCriticalSection(&g_csThreadCode);

  system("pause");
  return 0;
}

當然了事件物件同樣可以實現更為複雜的同步機制,在如下我們在建立物件時,可以設定non-signaled狀態執行的auto-reset模式,當我們設定好我們需要的引數時,可以直接使用SetEvent(hEvent)設定事件狀態,則會自動執行執行緒函式。

要建立一個manual-reset模式並且初始狀態為not-signaled的事件物件,需要按照以下步驟:

首先定義一個SECURITY_ATTRIBUTES結構體變數,設定其中的引數為NULL表示使用預設安全描述符,例如。

SECURITY_ATTRIBUTES sa = {0};
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = FALSE;

接著呼叫CreateEvent函式建立事件物件,將bManualResetbInitialState引數設定為FALSE,表示建立manual-reset模式的事件物件並初始狀態為not-signaled。例如:

HANDLE hEvent = CreateEvent(
                      &sa,           // 安全屬性
                      TRUE,          // Manual-reset模式
                      FALSE,         // Not-signaled 初始狀態
                      NULL           // 事件物件名稱
                      );

這樣,我們就建立了一個名為hEventmanual-reset模式的事件物件,初始狀態為not-signaled。可以透過SetEvent函式將事件物件設定為signaled狀態,透過ResetEvent函式將事件物件設定為non-signaled狀態,也可以透過WaitForSingleObject或者WaitForMultipleObjects函式等待事件物件的狀態變化。

#include <windows.h>  
#include <stdio.h>  
#include <process.h>  
#define STR_LEN 100  

// 儲存全域性字串
static char str[STR_LEN];

// 設定事件控制程式碼
static HANDLE hEvent;

// 統計字串中是否存在A
unsigned WINAPI NumberOfA(void *arg)
{
  int cnt = 0;
  // 等待執行緒物件事件
  WaitForSingleObject(hEvent, INFINITE);
  for (int i = 0; str[i] != 0; i++)
  {
    if (str[i] == 'A')
      cnt++;
  }
  printf("Num of A: %d \n", cnt);
  return 0;
}

// 統計字串總長度
unsigned WINAPI NumberOfOthers(void *arg)
{
  int cnt = 0;
  // 等待執行緒物件事件
  WaitForSingleObject(hEvent, INFINITE);
  for (int i = 0; str[i] != 0; i++)
  {
    if (str[i] != 'A')
      cnt++;
  }
  printf("Num of others: %d \n", cnt - 1);
  return 0;
}

int main(int argc, char *argv[])
{
  HANDLE hThread1, hThread2;

  // 以non-signaled建立manual-reset模式的事件物件
  // 該物件建立後不會被立即執行,只有我們設定狀態為Signaled時才會繼續
  hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

  hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);
  hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);

  fputs("Input string: ", stdout);
  fgets(str, STR_LEN, stdin);

  // 字串讀入完畢後,將事件控制程式碼改為signaled狀態  
  SetEvent(hEvent);

  WaitForSingleObject(hThread1, INFINITE);
  WaitForSingleObject(hThread2, INFINITE);

  // non-signaled 如果不更改,物件繼續停留在signaled
  ResetEvent(hEvent);

  CloseHandle(hEvent);

  system("pause");
  return 0;
}

9.2.2 CreateSemaphore

CreateSemaphore 是Windows API提供的用於建立訊號量的函式之一,用於控制多個執行緒之間對共享資源的訪問數量。該函式常用於建立一個計數訊號量物件,並返回一個表示該訊號量物件的控制程式碼。可以透過ReleaseSemaphore函式將該訊號量物件的計數加1,透過WaitForSingleObject或者WaitForMultipleObjects函式等待訊號量物件的計數變成正數以後再將其減1,以實現對共享資源訪問數量的控制。

CreateSemaphore 函式常用於實現生產者消費者模型、執行緒池、任務佇列等併發程式設計場景,用於限制訪問共享資源的執行緒數量。訊號量機制更多時候被用於限制資源的數量而不是限制執行緒的數量,但也可以用來實現一些執行緒同步場景。

該函式的函式原型如下:

HANDLE CreateSemaphore(
  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
  LONG                  lInitialCount,
  LONG                  lMaximumCount,
  LPCTSTR               lpName
);

引數說明:

  • lpSemaphoreAttributes:指向SECURITY_ATTRIBUTES結構體的指標,指定訊號量物件的安全描述符和訪問許可權。通常設為NULL,表示使用預設值。
  • lInitialCount:指定訊號量物件的初始計數,表示可以同時訪問共享資源的執行緒數量。
  • lMaximumCount:指定訊號量物件的最大計數,表示訊號量物件的計數上限。
  • lpName:指定訊號量物件的名稱,可以為NULL。

總的來說,CreateSemaphore 是實現執行緒同步和程式通訊,控制對共享資源的訪問數量的重要手段之一,如下一段演示程式碼片段則透過此方法解決了執行緒透過問題,首先呼叫CreateSemaphore初始化時將訊號量設定一個最大值,每次進入執行緒函式內部時,則ReleaseSemaphore訊號自動加1,如果大於指定的數值則WaitForSingleObject等待釋放訊號.

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

// 全域性資源
long g_nNum = 0;

// 子執行緒個數
const int THREAD_NUM = 10;

CRITICAL_SECTION  g_csThreadCode;
HANDLE g_hThreadParameter;

unsigned int __stdcall ThreadFunction(void *ptr)
{
  int nThreadNum = *(int *)ptr;

  // 訊號量++
  ReleaseSemaphore(g_hThreadParameter, 1, NULL);

  // 進入執行緒鎖
  EnterCriticalSection(&g_csThreadCode);
  g_nNum++;
  printf("執行緒編號: %d --> 全域性資源值: %d --> 子執行緒ID: %d \n", nThreadNum, g_nNum, GetCurrentThreadId());

  // 離開執行緒鎖
  LeaveCriticalSection(&g_csThreadCode);
  return 0;
}

int main(int argc,char * argv[])
{
  unsigned int ThreadCount = 0;
  HANDLE  handle[THREAD_NUM];

  // 初始化訊號量當前0個資源,最大允許1個同時訪問
  g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL);
  InitializeCriticalSection(&g_csThreadCode);

  for (int each = 0; each < THREAD_NUM; each++)
  {
    handle[each] = (HANDLE)_beginthreadex(NULL, 0, ThreadFunction, &each, 0, &ThreadCount);

    // 等待訊號量>0
    WaitForSingleObject(g_hThreadParameter, INFINITE);
  }

  // 關閉訊號
  CloseHandle(g_hThreadParameter);

  // 等待所有程式結束
  WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
  DeleteCriticalSection(&g_csThreadCode);

  system("pause");
  return 0;
}

如下所示程式碼片段,是一個應用了兩個執行緒的案例,初始化訊號為0,利用訊號量值為0時進入non-signaled狀態,大於0時進入signaled狀態的特性即可實現執行緒同步。

執行WaitForSingleObject(semTwo, INFINITE);會讓執行緒函式進入類似掛起的狀態,當接到ReleaseSemaphore(semOne, 1, NULL);才會恢復執行。

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

static HANDLE semOne,semTwo;
static int num;

// 執行緒函式A用於接收參書
DWORD WINAPI ReadNumber(LPVOID lpParamter)
{
  int i;
  for (i = 0; i < 5; i++)
  {
    fputs("Input Number: ", stdout);

    // 臨界區的開始 signaled狀態  
    WaitForSingleObject(semTwo, INFINITE);
    
    scanf("%d", &num);

    // 臨界區的結束 non-signaled狀態  
    ReleaseSemaphore(semOne, 1, NULL);
  }
  return 0;
}

// 執行緒函式B: 使用者接受引數後完成計算
DWORD WINAPI Check(LPVOID lpParamter)
{
  int sum = 0, i;
  for (i = 0; i < 5; i++)
  {
    // 臨界區的開始 non-signaled狀態  
    WaitForSingleObject(semOne, INFINITE);
    sum += num;

    // 臨界區的結束 signaled狀態  
    ReleaseSemaphore(semTwo, 1, NULL);
  }
  printf("The Number IS: %d \n", sum);
  return 0;
}

int main(int argc, char *argv[])
{
  HANDLE hThread1, hThread2;

  // 建立訊號量物件,設定為0進入non-signaled狀態  
  semOne = CreateSemaphore(NULL, 0, 1, NULL);

  // 建立訊號量物件,設定為1進入signaled狀態  
  semTwo = CreateSemaphore(NULL, 1, 1, NULL);

  hThread1 = CreateThread(NULL, 0, ReadNumber, NULL, 0, NULL);
  hThread2 = CreateThread(NULL, 0, Check, NULL, 0, NULL);

  // 關閉臨界區
  WaitForSingleObject(hThread1, INFINITE);
  WaitForSingleObject(hThread2, INFINITE);

  CloseHandle(semOne);
  CloseHandle(semTwo);

  system("pause");
  return 0;
}

9.2.3 CreateMutex

CreateMutex 是Windows API提供的用於建立互斥體物件的函式之一,該函式用於建立一個互斥體物件,並返回一個表示該互斥體物件的控制程式碼。可以透過WaitForSingleObject或者WaitForMultipleObjects函式等待互斥體物件,以確保只有一個執行緒能夠訪問共享資源,其他執行緒需要等待該執行緒釋放互斥體物件後才能繼續訪問。當需要釋放互斥體物件時,可以呼叫ReleaseMutex函式將其釋放。

CreateMutex 函式常用於對共享資源的訪問控制,避免多個執行緒同時訪問導致資料不一致的問題。有時候,互斥體也被用於跨程式同步訪問共享資源。

該函式的函式原型如下:

HANDLE CreateMutex(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,
  BOOL                  bInitialOwner,
  LPCTSTR               lpName
);

引數說明:

  • lpMutexAttributes:指向SECURITY_ATTRIBUTES結構體的指標,指定互斥體物件的安全描述符和訪問許可權。通常設為NULL,表示使用預設值。
  • bInitialOwner:指定互斥體的初始狀態,TRUE表示將互斥體設定為有所有權的狀態,FALSE表示將互斥體設定為沒有所有權的狀態。
  • lpName:指定互斥體的名稱,可以為NULL。

該函式是實現執行緒同步和程式通訊,控制對共享資源的訪問的重要手段之一,應用廣泛且易用。

如下案例所示,使用互斥鎖可以實現單位時間內,只允許一個執行緒擁有對共享資源的獨佔許可權,從而實現了互不衝突的執行緒同步。

#include <windows.h>
#include <iostream>

using namespace std;

// 建立互斥鎖
HANDLE hMutex = NULL;

// 執行緒函式
DWORD WINAPI Func(LPVOID lpParamter)
{
  for (int x = 0; x < 10; x++)
  {
    // 請求獲得一個互斥鎖
    WaitForSingleObject(hMutex, INFINITE);

    cout << "thread func" << endl;

    // 釋放互斥鎖
    ReleaseMutex(hMutex);
  }
  return 0;
}

int main(int argc,char * argv[])
{
  HANDLE hThread = CreateThread(NULL, 0, Func, NULL, 0, NULL);

  hMutex = CreateMutex(NULL, FALSE, "lyshark");
  CloseHandle(hThread);

  for (int x = 0; x < 10; x++)
  {
    // 請求獲得一個互斥鎖
    WaitForSingleObject(hMutex, INFINITE);
    cout << "main thread" << endl;
    
    // 釋放互斥鎖
    ReleaseMutex(hMutex);
  }
  system("pause");
  return 0;
}

當然透過互斥鎖我們也可以實現贊單位時間內同時同步執行兩個執行緒函式,如下程式碼所示;

#include <windows.h>
#include <iostream>

using namespace std;

// 建立互斥鎖
HANDLE hMutex = NULL;
#define NUM_THREAD 50

// 執行緒函式1
DWORD WINAPI FuncA(LPVOID lpParamter)
{
  for (int x = 0; x < 10; x++)
  {
    // 請求獲得一個互斥鎖
    WaitForSingleObject(hMutex, INFINITE);

    cout << "this is thread func A" << endl;

    // 釋放互斥鎖
    ReleaseMutex(hMutex);
  }
  return 0;
}

// 執行緒函式2
DWORD WINAPI FuncB(LPVOID lpParamter)
{
  for (int x = 0; x < 10; x++)
  {
    // 請求獲得一個互斥鎖
    WaitForSingleObject(hMutex, INFINITE);

    cout << "this is thread func B" << endl;

    // 釋放互斥鎖
    ReleaseMutex(hMutex);
  }
  return 0;
}

int main(int argc, char * argv[])
{

  // 用來儲存執行緒函式的控制程式碼
  HANDLE tHandle[NUM_THREAD];

  // 建立互斥量,此時為signaled狀態
  hMutex = CreateMutex(NULL, FALSE, "lyshark");

  for (int x = 0; x < NUM_THREAD; x++)
  {
    if (x % 2)
    {
      tHandle[x] = CreateThread(NULL, 0, FuncA, NULL, 0, NULL);
    }
    else
    {
      tHandle[x] = CreateThread(NULL, 0, FuncB, NULL, 0, NULL);
    }
  }

  // 等待所有執行緒函式執行完畢
  WaitForMultipleObjects(NUM_THREAD, tHandle, TRUE, INFINITE);
  
  // 銷燬互斥物件
  CloseHandle(hMutex);

  system("pause");
  return 0;
}

9.2.4 ThreadParameters

線上程環境中,有時候啟動新執行緒時我們需要對不同的執行緒傳入不同的引數,通常實現執行緒傳參的方法有許多,一般可分為使用全域性變數,使用結構體,使用類的成員函式等,本節將使用結構體傳參,透過建立一個結構體,將需要傳遞的引數儲存在結構體中,並將結構體的指標傳遞給執行緒函式。子執行緒在執行時,可以透過該指標訪問結構體中的引數。

對於簡單的引數傳遞而言,執行緒函式中定義LPVOID允許傳遞一個引數,此時我們只需要在函式中接收並強轉(int)(LPVOID)port即可獲取到一個整數型別的引數,如下是一個簡單的埠掃描軟體程式碼片段。

#include <stdio.h>
#include <Windows.h>

// 執行緒函式接收一個引數
DWORD WINAPI ScanThread(LPVOID port)
{
  // 將引數強制轉化為需要的型別
  int Port = (int)(LPVOID)port;
  printf("[+] 埠: %5d \n", port);
  return 1;
}

int main(int argc, char* argv[])
{
  HANDLE handle;

  for (int port = 0; port < 100; port++)
  {
    handle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ScanThread, (LPVOID)port, 0, 0);
  }
  WaitForSingleObject(handle, INFINITE);

  system("pause");
  return 0;
}

當然只傳遞一個引數在多數情況下時不夠使用的,而想線上程函式中傳遞多個引數,則需要傳遞一個結構指標,透過執行緒函式內部強轉為結構型別後,即可實現取值,如下程式碼中我們首先定義了一個THREAD_PARAM結構體,該結構內有兩個成員分別指定掃描主機地址以及埠號,當引數被傳遞到ScanThread執行緒函式內部時只需要將指標內的資料複製到自身執行緒函式內,即可正確的引用特定的引數。

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

typedef struct _THREAD_PARAM
{
  char *HostAddr;             // 掃描主機
  DWORD dwStartPort;          // 埠號
}THREAD_PARAM;

// 這個掃描執行緒函式
DWORD WINAPI ScanThread(LPVOID lpParam)
{
  // 複製傳遞來的掃描引數
  THREAD_PARAM ScanParam = { 0 };
  MoveMemory(&ScanParam, lpParam, sizeof(THREAD_PARAM));
  printf("地址: %-16s --> 埠: %-5d \n", ScanParam.HostAddr, ScanParam.dwStartPort);
  return 0;
}

int main(int argc, char *argv[])
{
  THREAD_PARAM ThreadParam = { 0 };

  for (int ip = 0; ip < 100; ip++)
  {
    char src_addr[50] = "192.168.1.";
    char sub_addr[10] = {0};
    // int number = atoi(sub_addr);
    
    // 將整數轉為字串
    sprintf(sub_addr, "%d", ip);
    strcat(src_addr, sub_addr);

    // 將拼接好的字串放到HostAddr
    ThreadParam.HostAddr = src_addr;

    for (DWORD port = 1; port < 10; port++)
    {
      // 指定埠號
      ThreadParam.dwStartPort = port;
      HANDLE hThread = CreateThread(NULL, 0, ScanThread, (LPVOID)&ThreadParam, 0, NULL);
      WaitForSingleObject(hThread, INFINITE);
    }
  }

  system("pause");
  return 0;
}

9.2.5 ThreadPool

Windows 執行緒池是一種非同步執行任務的機制,可以將任務提交到執行緒池中,由執行緒池自動分配執行緒執行任務。執行緒池可以有效地利用系統資源,提高程式的併發能力和效能。Windows 執行緒池是Windows作業系統提供的一種原生的執行緒池機制,可以使用Windows API函式進行操作。

CreateThreadpoolWork 是Windows API提供的用於建立一個工作從池執行緒中執行的工作物件的函式之一,該函式用於建立一個工作項,並返回一個表示該工作項的指標。可以透過SubmitThreadpoolWork函式將該工作項提交到執行緒池中進行執行。當該工作項完成後,執行緒池還可以使用回撥函式清理函式TP_FREE_CLEANUP_GROUP回收資源。

該函式的函式原型如下:

PTP_WORK CreateThreadpoolWork(
  PTP_WORK_CALLBACK  pfnwk,     // 工作項回撥函式指標
  PVOID              pv,        // 回撥函式的引數指標
  PTP_CALLBACK_ENVIRON pcbe      // 回撥函式執行環境
);

引數說明:

  • pfnwk:指向工作項回撥函式的指標,該函式將在工作執行緒池中執行。例如:
VOID CALLBACK MyWorkCallback(
    PTP_CALLBACK_INSTANCE Instance,
    PVOID Context, PTP_WORK Work)
{
    // 實現工作項的具體操作
}
  • pv:指向回撥函式的引數指標,由回撥函式進行處理。

  • pcbe:指向TP_CALLBACK_ENVIRON結構體的指標,提供了回撥函式需要的一些執行環境資訊,例如可選的回撥函式執行器TP_CALLBACK_INSTANCE和回撥函式完成後的清理函式TP_CLEANUP_GROUP等。如果為NULL,則使用系統提供的預設值。

CreateThreadpoolWork 函式常用於實現執行緒池中的任務佇列,可以將一個具體的任務封裝為一個工作項,並提交到執行緒池中等待執行。該機制可以有效地提高任務的處理速度和效率,減少系統資源開銷。但是,需要注意執行緒池的資源佔用問題,合理調優,避免執行緒洩漏等問題。

CallbackMayRunLong 是Windows API提供的呼叫標記函式之一,該函式用於標記回撥函式是否可能耗時較長。如果回撥函式不會耗時較長,則無需呼叫該函式。如果回撥函式可能耗時較長,則建議在執行回撥函式之前呼叫該函式對回撥函式進行標記,以便執行緒池進行資源分配和排程等策略。

CallbackMayRunLong函式的函式原型如下:

VOID CallbackMayRunLong(
  PTP_CALLBACK_INSTANCE pci
);

引數說明:

  • pci:指向TP_CALLBACK_INSTANCE結構體的指標,表示回撥函式的執行器,用於提供回撥函式的執行環境資訊。

SubmitThreadpoolWork 是Windows API提供的將工作項提交到執行緒池中執行的函式之一,該函式用於將工作項提交到執行緒池中等待被工作者執行緒執行。執行緒池中的工作者執行緒透過GetQueuedCompletionStatus函式從工作佇列中獲取工作項並執行。透過SubmitThreadpoolWorkGetQueuedCompletionStatus結合使用,可以實現執行緒池中的任務佇列,提高任務處理效率和系統效能。

SubmitThreadpoolWork 函式的函式原型如下:

VOID SubmitThreadpoolWork(
  PTP_WORK pwk
);

引數說明:

  • pwk:指向TP_WORK結構體的指標,表示要提交到執行緒池中執行的工作項。

讀者需要注意,SubmitThreadpoolWork 函式提交的是工作項而不是回撥函式,回撥函式是透過事先建立工作項指定的。在使用SubmitThreadpoolWork提交工作項時,需要根據具體的業務需求進行合理的設計和實現,避免執行緒池資源浪費、效能下降、記憶體洩漏等問題。

WaitForThreadpoolWorkCallbacks 是Windows API提供的等待執行緒池中工作項完成的函式之一,該函式用於等待執行緒池中提交的所有工作項被處理完畢。需要注意的是,該函式會阻塞當前執行緒直到所有工作項處理完畢,因此需要謹慎使用,避免阻塞其它執行緒的執行。

WaitForThreadpoolWorkCallbacks 函式的函式原型如下:

VOID WaitForThreadpoolWorkCallbacks(
  PTP_WORK pwk,
  BOOL     fCancelPendingCallbacks
);

引數說明:

  • pwk:指向TP_WORK結構體的指標,表示要等待完成的工作項。
  • fCancelPendingCallbacks:用於指定是否取消所有待處理的工作項。如果為TRUE,則取消所有待處理的工作項;如果為FALSE,則等待所有待處理的工作項被處理完畢。

要使用CreateThreadpoolWork()建立一個執行緒池很容易實現,讀者只需要指定TaskHandler執行緒函式即可,當需要啟動執行緒池時透過呼叫SubmitThreadpoolWork函式提交一組請求即可,如下是一個簡單的執行緒池建立功能實現。

#include <Windows.h>
#include <iostream>
#include <stdlib.h>

unsigned long g_count = 0;

// 執行緒執行函式
void NTAPI TaskHandler(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work)
{
  if (CallbackMayRunLong(Instance))
  {
    printf("剩餘資源: %d --> 執行緒ID: %d \n", InterlockedIncrement(&g_count), GetCurrentThreadId());
  }

  Sleep(5000);
  printf("執行子執行緒 \n");

  for (int x = 0; x < 100; x++)
  {
    printf("執行緒ID: %d ---> 子執行緒: %d \n", GetCurrentThreadId(), x);
  }
}

int main(int argc,char *argv)
{
  PTP_WORK workItem = CreateThreadpoolWork(TaskHandler, NULL, NULL);

  for (int x = 0; x < 100; x++)
  {
    // 呼叫SubmitThreadpoolWork向執行緒池提交一個請求
    SubmitThreadpoolWork(workItem);
  }

  // 等待執行緒池呼叫結束
  WaitForThreadpoolWorkCallbacks(workItem, false);
  CloseThreadpoolWork(workItem);

  system("pause");
  return 0;
}

執行緒池函式同樣支援限制執行緒數,限制執行緒可以透過呼叫SetThreadpoolThreadMinimum()實現,該函式可以在建立執行緒池後設定執行緒池的最小執行緒數。當執行緒池中的任務佇列中存在待執行的任務,並且當前工作執行緒的數量小於最小執行緒數時,執行緒池將自動建立新的工作執行緒,以確保待執行任務能夠及時得到處理。

以下是函式的原型定義:

VOID WINAPI SetThreadpoolThreadMinimum(
  PTP_POOL ptpp,
  DWORD     cthrdMic
);

引數說明:

  • ptpp:指向執行緒池物件的指標。
  • cthrdMic:執行緒池中的最小執行緒數。

執行緒池也支援分組操作,可透過繫結TP_CALLBACK_ENVIRON執行緒池環境變數實現分組,TP_CALLBACK_ENVIRON是Windows執行緒池API的一部分,它是一個環境變數結構體,用於確定要呼叫的執行緒池回撥函式的環境。

以下是TP_CALLBACK_ENVIRON結構體的定義:

typedef struct _TP_CALLBACK_ENVIRON {
  TP_VERSION                  Version;
  PTP_POOL                    Pool;
  PTP_CLEANUP_GROUP           CleanupGroup;
  PFN_TP_SIMPLE_CALLBACK      CleanupGroupCancelCallback;
  PVOID                       RaceDll;
  struct _ACTIVATION_CONTEXT *ActivationContext;
  PFN_IO_CALLBACK             FinalizationCallback;
  union {
    DWORD Flags;
    struct {
      DWORD LongFunction : 1;
      DWORD Persistent   : 1;
      DWORD Private      : 30;
    } DUMMYSTRUCTNAME;
  } DUMMYUNIONNAME;
} TP_CALLBACK_ENVIRON, *PTP_CALLBACK_ENVIRON;

主要成員說明:

  • Version:回撥環境的版本,必須為 TP_VERSION。
  • Pool:回撥環境所屬的執行緒池物件。
  • CleanupGroup:回撥環境所屬的清理組物件,用於控制回撥的取消和資源管理。
  • CleanupGroupCancelCallback:當清理組取消回撥時,所呼叫的回撥函式。
  • RaceDll:保留欄位,用於標記已經看過這個環境變數的 DLL。
  • ActivationContext:回撥環境的啟用上下文,用來保證回撥中需要的外部資源正確載入。
  • FinalizationCallback:當回撥函式執行完成後呼叫的函式。
  • Flags:回撥環境的標誌,用於設定回撥函式的屬性。

使用TP_CALLBACK_ENVIRON結構體,可以在建立執行緒池回撥函式時,配置回撥函式的環境和引數,以控制回撥函式的執行方式和行為。

例如,可以使用TP_CALLBACK_ENVIRON中的CleanupGroupCleanupGroupCancelCallback成員,將回撥函式新增到清理組中,並在需要時取消回撥。又或者在FinalizationCallback中執行某些特殊的清理任務,以確保在回撥函式執行完畢後釋放資源。

#include <Windows.h>
#include <iostream>
#include <stdlib.h>

// 執行緒執行函式
void NTAPI TaskHandler(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work)
{
  for (int x = 0; x < 100; x++)
  {
    printf("執行緒ID: %d ---> 子執行緒: %d \n", GetCurrentThreadId(), x);
  }
}

// 單次執行緒任務
void NTAPI poolThreadFunc(PTP_CALLBACK_INSTANCE Instance, PVOID Context)
{
  printf("執行單次執行緒任務: %d \n", GetCurrentThreadId());
}

int main(int argc,char *argv)
{
  // 建立執行緒池
  PTP_POOL pool = CreateThreadpool(NULL);

  // 設定執行緒池 最小與最大 資源數
  SetThreadpoolThreadMinimum(pool, 1);
  SetThreadpoolThreadMaximum(pool, 100);

  // 初始化執行緒池環境變數
  TP_CALLBACK_ENVIRON cbe;

  InitializeThreadpoolEnvironment(&cbe);

  // 設定執行緒池回撥的執行緒池
  SetThreadpoolCallbackPool(&cbe, pool);

  // 建立清理組
  PTP_CLEANUP_GROUP cleanupGroup = CreateThreadpoolCleanupGroup();

  // 為執行緒池設定清理組
  SetThreadpoolCallbackCleanupGroup(&cbe, cleanupGroup, NULL);

  // 建立執行緒池
  PTP_WORK pwork = CreateThreadpoolWork((PTP_WORK_CALLBACK)TaskHandler, NULL, &cbe);

  // 迴圈提交執行緒工作任務
  for (int x = 0; x < 100; x++)
  {
    SubmitThreadpoolWork(pwork);
  }

  // 提交單次執行緒任務
  TrySubmitThreadpoolCallback(poolThreadFunc, NULL, &cbe);
  TrySubmitThreadpoolCallback(poolThreadFunc, NULL, &cbe);

  // 等待執行緒池結束,關閉執行緒組
  WaitForThreadpoolWorkCallbacks(pwork, false);
  CloseThreadpoolWork(pwork);

  // 關閉清理組
  CloseThreadpoolCleanupGroupMembers(cleanupGroup, false, NULL);
  
  // 銷燬執行緒池環境變數
  DestroyThreadpoolEnvironment(&cbe);

  CloseThreadpool(pool);
  system("pause");
  return 0;
}

當讀者使用執行緒池時,同樣會遇到執行緒的同步問題,執行緒池內的執行緒函式同樣支援互斥鎖、訊號量、核心事件控制、臨界區控制等同步和互斥機制,用於保護共享資源的訪問和修改。

這些同步和互斥機制可以用來解決執行緒間競爭和資料不一致的問題。例如,線上程池中如果有多個工作執行緒同時訪問共享資源,就需要使用互斥鎖或臨界區控制來確保每個執行緒對共享資源的使用不會相互干擾,避免出現資料競爭和不一致的情況。

使用這些同步和互斥機制時,應該根據實際場景進行選擇和設計。例如,互斥鎖適合用於保護少量的共享資源、需要經常訪問和更新的場景,而訊號量適合用於控制併發訪問數量、資源池、生產者消費者模式等場景。同時,需要注意遵循執行緒安全和同步的原則,以避免死鎖、飢餓等問題。

#include <Windows.h>
#include <iostream>
#include <stdlib.h>

unsigned long g_count = 0;

// --------------------------------------------------------------
// 執行緒池同步-互斥量同步
// --------------------------------------------------------------
void NTAPI TaskHandlerMutex(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work)
{
  // 鎖定資源
  WaitForSingleObject(*(HANDLE *)Context, INFINITE);

  for (int x = 0; x < 100; x++)
  {
    printf("執行緒ID: %d ---> 子執行緒: %d \n", GetCurrentThreadId(), x);
    g_count = g_count + 1;
  }

  // 解鎖資源
  ReleaseMutexWhenCallbackReturns(Instance, *(HANDLE*)Context);
}

void TestMutex()
{
  // 建立互斥量
  HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);

  PTP_WORK pool = CreateThreadpoolWork((PTP_WORK_CALLBACK)TaskHandlerMutex, &hMutex, NULL);

  for (int i = 0; i < 1000; i++)
  {
    SubmitThreadpoolWork(pool);
  }

  WaitForThreadpoolWorkCallbacks(pool, FALSE);
  CloseThreadpoolWork(pool);
  CloseHandle(hMutex);

  printf("相加後 ---> %d \n", g_count);
}

// --------------------------------------------------------------
// 執行緒池同步-事件核心物件
// --------------------------------------------------------------
void NTAPI TaskHandlerKern(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work)
{
  // 鎖定資源
  WaitForSingleObject(*(HANDLE *)Context, INFINITE);

  for (int x = 0; x < 100; x++)
  {
    printf("執行緒ID: %d ---> 子執行緒: %d \n", GetCurrentThreadId(), x);
    g_count = g_count + 1;
  }

  // 解鎖資源
  SetEventWhenCallbackReturns(Instance, *(HANDLE*)Context);
}

void TestKern()
{
  HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
  SetEvent(hEvent);

  PTP_WORK pwk = CreateThreadpoolWork((PTP_WORK_CALLBACK)TaskHandlerKern, &hEvent, NULL);

  for (int i = 0; i < 1000; i++)
  {
    SubmitThreadpoolWork(pwk);
  }

  WaitForThreadpoolWorkCallbacks(pwk, FALSE);
  CloseThreadpoolWork(pwk);

  printf("相加後 ---> %d \n", g_count);
}

// --------------------------------------------------------------
// 執行緒池同步-訊號量同步
// --------------------------------------------------------------
void NTAPI TaskHandlerSemaphore(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work)
{
  // 鎖定資源
  WaitForSingleObject(*(HANDLE *)Context, INFINITE);

  for (int x = 0; x < 100; x++)
  {
    printf("執行緒ID: %d ---> 子執行緒: %d \n", GetCurrentThreadId(), x);
    g_count = g_count + 1;
  }

  // 解鎖資源
  ReleaseSemaphoreWhenCallbackReturns(Instance, *(HANDLE*)Context, 1);
}

void TestSemaphore()
{
  // 建立訊號量為100
  HANDLE hSemaphore = CreateSemaphore(NULL, 0, 100, NULL);

  ReleaseSemaphore(hSemaphore, 10, NULL);

  PTP_WORK pwk = CreateThreadpoolWork((PTP_WORK_CALLBACK)TaskHandlerSemaphore, &hSemaphore, NULL);

  for (int i = 0; i < 1000; i++)
  {
    SubmitThreadpoolWork(pwk);
  }

  WaitForThreadpoolWorkCallbacks(pwk, FALSE);
  CloseThreadpoolWork(pwk);
  CloseHandle(hSemaphore);

  printf("相加後 ---> %d \n", g_count);
}

// --------------------------------------------------------------
// 執行緒池同步-臨界區
// --------------------------------------------------------------
void NTAPI TaskHandlerLeave(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work)
{
  // 鎖定資源
  EnterCriticalSection((CRITICAL_SECTION*)Context);

  for (int x = 0; x < 100; x++)
  {
    printf("執行緒ID: %d ---> 子執行緒: %d \n", GetCurrentThreadId(), x);
    g_count = g_count + 1;
  }

  // 解鎖資源
  LeaveCriticalSectionWhenCallbackReturns(Instance, (CRITICAL_SECTION*)Context);
}

void TestLeave()
{
  CRITICAL_SECTION cs;
  InitializeCriticalSection(&cs);

  PTP_WORK pwk = CreateThreadpoolWork((PTP_WORK_CALLBACK)TaskHandlerLeave, &cs, NULL);

  for (int i = 0; i < 1000; i++)
  {
    SubmitThreadpoolWork(pwk);
  }

  WaitForThreadpoolWorkCallbacks(pwk, FALSE);
  DeleteCriticalSection(&cs);
  CloseThreadpoolWork(pwk);

  printf("相加後 ---> %d \n", g_count);
}

int main(int argc,char *argv)
{
  // TestMutex();
  // TestKern();
  // TestSemaphore();
  TestLeave();

  system("pause");
  return 0;
}

本文作者: 王瑞
本文連結: https://www.lyshark.com/post/505839cb.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協議。轉載請註明出處!

相關文章