9.1 運用API建立多執行緒

lyshark發表於2023-10-01

在Windows平臺下建立多執行緒有兩種方式,讀者可以使用CreateThread函式,或者使用beginthreadex函式均可,兩者雖然都可以用於建立多執行緒環境,但還是存在一些差異的,首先CreateThread函式它是Win32 API的一部分,而_beginthreadexC/C++執行庫的一部分,在引數返回值型別方面,CreateThread返回執行緒控制程式碼,而_beginthreadex返回執行緒ID,當然這兩者在使用上並沒有太大的差異,但為了程式碼更加通用筆者推薦使用後者,因為後者與平臺無關性更容易實現跨平臺需求。

9.1.1 CreateThread

CreateThread 函式是Windows API提供的用於建立執行緒的函式。它接受一些引數,如執行緒的入口函式、執行緒的堆疊大小等,可以建立一個新的執行緒並返回執行緒控制程式碼。開發者可以使用該控制程式碼控制該執行緒的執行狀態。需要注意,在使用CreateThread建立執行緒時,執行緒入口函式的返回值是執行緒的退出碼,而不是執行緒執行的結果值。

CreateThread 函式原型如下:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                  dwStackSize,
  LPTHREAD_START_ROUTINE  lpStartAddress,
  LPVOID                  lpParameter,
  DWORD                   dwCreationFlags,
  LPDWORD                 lpThreadId
);

引數說明:

  • lpThreadAttributes:指向SECURITY_ATTRIBUTES結構體的指標,指定執行緒安全描述符和訪問許可權。通常設為NULL,表示使用預設值。
  • dwStackSize:指定執行緒堆疊的大小,以位元組為單位。如果dwStackSize為0,則使用預設的堆疊大小。(注:在32位程式下,該值的預設大小為1MB;在64位程式下,該值的預設大小為4MB)
  • lpStartAddress:指向執行緒函式的指標,這個函式就是執行緒執行的入口點。當執行緒啟動時,系統就會呼叫這個函式。
  • lpParameter:指定傳遞給執行緒函式的引數,可以為NULL。
  • dwCreationFlags:指定執行緒的建立標誌。通常設為0,表示使用預設值。
  • lpThreadId:指向一個DWORD變數的指標,表示返回的執行緒ID號。可以為NULL。

CreateThread 函式將建立一個新的執行緒,並返回執行緒控制程式碼。開發者可以使用該控制程式碼控制該執行緒的執行狀態,如掛起、恢復、終止等。執行緒建立成功後,執行執行緒函式進行相應的業務處理。需要注意的是,在使用CreateThread建立執行緒時,執行緒入口函式的返回值是執行緒的退出碼,而不是執行緒執行的結果值。

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

using namespace std;

DWORD WINAPI Func(LPVOID lpParamter)
{
  for (int x = 0; x < 10; x++)
  {
    cout << "thread function" << endl;
    Sleep(200);
  }
  return 0;
}

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

  for (int x = 0; x < 10; x++)
  {
    cout << "main thread" << endl;
    Sleep(400);
  }

  system("pause");
  return 0;
}

如上所示程式碼中我們線上程函式Func()內沒有進行任何的加鎖操作,那麼也就會出現資源的爭奪現象,這些會被搶奪的資源就被稱為是臨界資源,我們可以透過設定臨界鎖來實現同一時刻內保持一個執行緒操作資源。

EnterCriticalSection 是Windows API提供的執行緒同步函式之一,用於進入一個臨界區並且鎖定該區域,以確保同一時間只有一個執行緒訪問臨界區程式碼。

EnterCriticalSection函式的函式原型如下:

void EnterCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
);

引數說明:

  • lpCriticalSection:指向CRITICAL_SECTION結構體的指標,表示要進入的臨界區。

EnterCriticalSection 函式將等待,直到指定的臨界區物件可用並且已經鎖定,然後,當前執行緒將進入臨界區。臨界區中的程式碼將在當前執行緒完成之前,不允許被任何其他執行緒執行。當執行緒完成臨界區的工作時,應該呼叫LeaveCriticalSection函式釋放臨界區。否則,其他執行緒將無法進入臨界區,導致死鎖。

EnterCriticalSection 函式是比較底層的執行緒同步函式,需要開發者自行建立臨界區,維護臨界區的狀態並進行加鎖解鎖的操作,使用時需要注意對臨界區中的操作進行適當的封裝和處理。同時,EnterCriticalSection函式也是比較高效的執行緒同步方式,對於需要頻繁訪問臨界資源的場景,可以透過使用臨界區來提高程式的效能。

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

int Global_One = 0;

// 全域性定義臨界區物件
CRITICAL_SECTION g_cs;

// 定義一個執行緒函式
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
  // 加鎖防止執行緒資料衝突
  EnterCriticalSection(&g_cs);
  for (int x = 0; x < 10; x++)
  {
    Global_One++;
    Sleep(1);
  }

  // 執行完修改以後,需要釋放鎖
  LeaveCriticalSection(&g_cs);
  return 0;
}

int main(int argc, char * argv[])
{
  // 初始化臨界區
  InitializeCriticalSection(&g_cs);
  HANDLE hThread[10] = { 0 };

  for (int x = 0; x < 10; x++)
  {
    // 迴圈建立執行緒
    hThread[x] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
  }

  // 等待多個執行緒執行結束
  WaitForMultipleObjects(10, hThread, TRUE, INFINITE);

  // 最後迴圈釋放資源
  for (int x = 0; x < 10; x++)
  {
    CloseHandle(hThread[x]);
  }

  printf("全域性變數值: %d \n", Global_One);

  // 釋放鎖
  DeleteCriticalSection(&g_cs);

  system("pause");
  return 0;
}

9.1.2 BeginThreadex

BeginThreadex 是C/C++執行庫提供的用於建立執行緒的函式。它也接受一些引數,如執行緒的入口函式、執行緒的堆疊大小等,與CreateThread不同的是,_beginthreadex函式返回的是執行緒的ID,而不是執行緒控制程式碼。開發者可以使用該ID在執行時控制該執行緒的執行狀態。此外,_beginthreadex函式通常與_endthreadex配對使用,供執行緒退出時使用。

beginthreadex 函式的函式原型如下:

uintptr_t _beginthreadex(
  void*                 security,
  unsigned             stack_size,
  unsigned(__stdcall*  start_address)(void*),
  void*                 arglist,
  unsigned             initflag,
  unsigned*            thrdaddr
);

引數說明:

  • security:與Windows安全機制相關,用於指定執行緒的安全屬性,一般填NULL即可。
  • stack_size:指定執行緒的堆疊大小,以位元組為單位。如果stack_size為0,則使用預設的堆疊大小。
  • start_address:執行緒函式的入口點。
  • arglist:傳遞給執行緒函式的引數。
  • initflag:執行緒標誌,0表示啟動執行緒後立即執行,CREATE_SUSPENDED表示啟動執行緒後暫停執行。
  • thrdaddr:指向unsigned變數的指標,表示返回的執行緒ID號。可以為NULL。

CreateThread相比,_beginthreadex函式返回執行緒ID而非執行緒控制程式碼,使用時需要注意區分。與CreateThread不同的是,_beginthreadex函式接受傳遞給執行緒函式的引數放在arglist中,方便傳遞多個引數。執行緒使用完需要呼叫_endthreadex函式來關閉執行緒。當使用了_beginthreadex建立的執行緒退出時,會呼叫_endthreadex來結束執行緒,這裡的返回值會被當做執行緒的退出碼。

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

using namespace std;

unsigned WINAPI Func(void *arg)
{
  for (int x = 0; x < 10; x++)
  {
    cout << "thread function" << endl;
    Sleep(200);
  }
  return 0;
}

int main(int argc, char * argv[])
{
  HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, Func, NULL, 0, NULL);
  CloseHandle(hThread);
  for (int x = 0; x < 10; x++)
  {
    cout << "main thread" << endl;
    Sleep(400);
  }

  system("pause");
  return 0;
}

由於CreateThread()函式是Windows提供的API介面,在C/C++語言另有一個建立執行緒的函式_beginthreadex()該函式在建立新執行緒時會分配並初始化一個_tiddata塊,這個塊用來存放一些需要執行緒獨享的資料,從而保證了執行緒資源不會發生衝突的情況,程式碼只需要稍微在上面基礎上改進即可。

當然該函式同樣需要設定執行緒臨界區而設定方式與CreateThread中所展示的完全一致。

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

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

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

CRITICAL_SECTION  g_csThreadCode;

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

  // 進入執行緒鎖
  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];

  InitializeCriticalSection(&g_csThreadCode);

  for (int each = 0; each < THREAD_NUM; each++)
  {
    handle[each] = (HANDLE)_beginthreadex(NULL, 0, ThreadFunction, &each, 0, &ThreadCount);
    printf("執行緒ID: %d \n", ThreadCount);
  }

  WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);

  DeleteCriticalSection(&g_csThreadCode);

  system("pause");
  return 0;
}

總的來說,_beginthreadexCreateThread更加高階,封裝了許多細節,使用起來更方便,特別是對於傳遞多個引數的情況下,可以更簡單地傳參。

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

相關文章