多線程CreateThread函數的使用(六個參數介紹)

function CreateThread(
  lpThreadAttributes: Pointer;           {安全設置}
  dwStackSize: DWORD;                    {堆棧大小}
  lpStartAddress: TFNThreadStartRoutine; {入口函數}
  lpParameter: Pointer;                  {函數參數}
  dwCreationFlags: DWORD;                {啓動選項}
  var lpThreadId: DWORD                  {輸出線程 ID }
): THandle; stdcall;                     {返回線程句柄}

1、返回值:返回線程句柄
"句柄" 類似指針, 但通過指針可讀寫對象, 通過句柄只是使用對象;
有句柄的對象一般都是系統級別的對象(或叫內核對象); 之所以給我們的是句柄而不是指針, 目的只有一個: "安全";
貌似通過句柄能做很多事情, 但一般把句柄提交到某個函數(一般是系統函數)後, 我們也就到此爲止很難了解更多了; 事實上是系統並不相信我們.

不管是指針還是句柄, 都不過是內存中的一小塊數據(一般用結構描述), 微軟並沒有公開句柄的結構細節, 猜一下它應該包括: 真實的指針地址、訪問權限設置、引用計數等等.

既然 CreateThread 可以返回一個句柄, 說明線程屬於 "內核對象".
實際上不管線程屬於哪個進程, 它們在系統的懷抱中是平等的; 在優先級(後面詳談)相同的情況下, 系統會在相同的時間間隔內來運行一下每個線程, 不過這個間隔很小很小, 以至於讓我們誤以爲程序是在不間斷地運行.

這時你應該有一個疑問: 系統在去執行其他線程的時候, 是怎麼記住前一個線程的數據狀態的?
有這樣一個結構 TContext, 它基本上是一個 CPU 寄存器的集合, 線程是數據就是通過這個結構切換的, 我們也可以通過 GetThreadContext 函數讀取寄存器看看.
附上這個結構 TContext(或叫: CONTEXT、_CONTEXT) 的定義:

PContext = ^TContext;
_CONTEXT = record
  ContextFlags: DWORD;
  Dr0: DWORD;
  Dr1: DWORD;
  Dr2: DWORD;
  Dr3: DWORD;
  Dr6: DWORD;
  Dr7: DWORD;
  FloatSave: TFloatingSaveArea;
  SegGs: DWORD;
  SegFs: DWORD;
  SegEs: DWORD;
  SegDs: DWORD;
  Edi: DWORD;
  Esi: DWORD;
  Ebx: DWORD;
  Edx: DWORD;
  Ecx: DWORD;
  Eax: DWORD;
  Ebp: DWORD;
  Eip: DWORD;
  SegCs: DWORD;
  EFlags: DWORD;
  Esp: DWORD;
  SegSs: DWORD;
end;

2、參數6:輸出線程ID
CreateThread 的最後一個參數是 "線程的 ID";
既然可以返回句柄, 爲什麼還要輸出這個 ID? 現在我知道的是:
1、線程的 ID 是唯一的; 而句柄可能不只一個, 譬如可以用 GetCurrentThread 獲取一個僞句柄、可以用 DuplicateHandle 複製一個句柄等等.
2、ID 比句柄更輕便.
在主線程中 GetCurrentThreadId、MainThreadID獲取的都是主線程的 ID.

MainInstance: Indicates the instance handle for the main executable.
Use MainInstance to obtain the instance handle for the main executable of an application. This is useful in applications that use runtime libraries or packages, when you need the handle for the executable rather than for the library.

3、參數5:啓動選項
CreateThread 的倒數第二個參數 dwCreationFlags(啓動選項) 有兩個可選值:
0: 線程建立後立即執行入口函數;
CREATE_SUSPENDED: 線程建立後會掛起等待.
ResumeThread 恢復線程的運行; SuspendThread 掛起線程.
這兩個函數的參數都是線程句柄, 返回值是執行前的掛起計數.
什麼是掛起計數?
SuspendThread 會給這個數 +1; ResumeThread 會給這個數 -1; 但這個數最小是 0.
當這個數 = 0 時, 線程會運行; > 0 時會掛起.
如果被 SuspendThread 多次, 同樣需要 ResumeThread 多次才能恢復線程的運行.
ResumeThread 和 SuspendThread 分別對應 TThread 的 Resume 和 Suspend 方法, 很好理解.

4、參數4:函數參數
線程入口函數的參數是個無類型指針(Pointer), 用它可以指定任何數據; 本例是把鼠標點擊窗體的座標傳遞給線程的入口函數, 每次點擊窗體都會創建一個線程.

5、參數3:入口函數指針
到了入口函數了, 學到這個地方, 我查了一個入口函數的標準定義, 這個函數的標準返回值應該是 DWORD, 不過這函數在 Delphi 的 System 單元定義的是: TThreadFunc = function(Parameter: Pointer): Integer; 我以後會盡量使用 DWORD 做入口函數的返回值.
這個返回值有什麼用呢?
等線程退出後, 我們用 GetExitCodeThread 函數獲取的退出碼就是這個返回值!
如果線程沒有退出, GetExitCodeThread 獲取的退出碼將是一個常量 STILL_ACTIVE (259); 這樣我們就可以通過退出碼來判斷線程是否已退出.
還有一個問題: 前面也提到過, 線程函數不能是某個類的方法! 假如我們非要線程去執行類中的一個方法能否實現呢?
儘管可以用 Addr(類名.方法名) 或 MethodAddress('published 區的方法名') 獲取類中方法的地址, 但都不能當做線程的入口函數, 原因可能是因爲類中的方法的地址是在實例化爲對象時動態分配的.
後來換了個思路, 其實很簡單: 在線程函數中再調用方法不就得了, 估計 TThread 也應該是這樣.
CreateThread 第三個參數是函數指針, 新線程建立後將立即執行該函數, 函數執行完畢, 系統將銷燬此線程從而結束多線程的故事.

6、參數2:堆棧大小
棧是私有的但堆是公用的

CreateThread 的第二個參數是分配給線程的堆棧大小.
這首先這可以讓我們知道: 每個線程都有自己獨立的堆棧(也擁有自己的消息隊列).
什麼是堆棧? 其實堆是堆、棧是棧, 有時 "棧" 也被叫做 "堆棧".
它們都是進程中的內存區域, 主要是存取方式不同(棧:先進後出; 堆:先進先出);
"棧"(或叫堆棧)適合存取臨時而輕便的變量, 主要用來儲存局部變量; 譬如 for i := 0 to 99 do 中的 i 就只能存於棧中, 你把一個全局的變量用於 for 循環計數是不可以的.
現在我們知道了線程有自己的 "棧", 並且在建立線程時可以分配棧的大小.
前面所有的例子中, 這個值都是 0, 這表示使用系統默認的大小, 默認和主線程棧的大小一樣, 如果不夠用會自動增長;
那主線程的棧有多大? 這個值是可以設定的: Project -> Options -> Delphi Compiler -> Linking(如圖)
棧是私有的但堆是公用的, 如果不同的線程都來使用一個全局變量有點亂套;
爲解決這個問題 Delphi 爲我們提供了一個類似 var 的 ThreadVar 關鍵字, 線程在使用 ThreadVar 聲明的全局變量時會在各自的棧中留一個副本, 這樣就解決了衝突. 不過還是儘量使用局部變量, 或者在繼承 TThread 時使用類的成員變量, 因爲 ThreadVar 的效率不好, 據說比局部變量能慢 10 倍.

7、參數1:安全設置
CreateThread 的第一個參數 lpThreadAttributes 是指向 TSecurityAttributes 結構的指針, 一般都是置爲 nil, 這表示沒有訪問限制; 該結構的定義是:

//TSecurityAttributes(又名: SECURITY_ATTRIBUTES、_SECURITY_ATTRIBUTES)
_SECURITY_ATTRIBUTES = record
  nLength: DWORD;                {結構大小}
  lpSecurityDescriptor: Pointer; {默認 nil; 這是另一個結構 TSecurityDescriptor 的指針}
  bInheritHandle: BOOL;          {默認 False, 表示不可繼承}
end;

//TSecurityDescriptor(又名: SECURITY_DESCRIPTOR、_SECURITY_DESCRIPTOR)
_SECURITY_DESCRIPTOR = record
  Revision: Byte;
  Sbz1: Byte;
  Control: SECURITY_DESCRIPTOR_CONTROL;
  Owner: PSID;
  Group: PSID;
  Sacl: PACL;
  Dacl: PACL;
end;


夠複雜的, 但我們在多線程編程時不需要去設置它們, 大都是使用默認設置(也就是賦值爲 nil).

我覺得有必要在此刻了解的是: 建立系統內核對象時一般都有這個屬性(TSecurityAttributes);
在接下來多線程的課題中要使用一些內核對象, 不如先盤點一下, 到時碰到這個屬性時給個 nil 即可, 不必再費神.

{建立事件}
function CreateEvent(
  lpEventAttributes: PSecurityAttributes; {!}
  bManualReset: BOOL;
  bInitialState: BOOL;
  lpName: PWideChar
): THandle; stdcall;

{建立互斥}
function CreateMutex(
  lpMutexAttributes: PSecurityAttributes; {!}
  bInitialOwner: BOOL;
  lpName: PWideChar
): THandle; stdcall;

{建立信號}
function CreateSemaphore(
  lpSemaphoreAttributes: PSecurityAttributes; {!}
  lInitialCount: Longint;
  lMaximumCount: Longint;
  lpName: PWideChar
): THandle; stdcall;

{建立等待計時器}
function CreateWaitableTimer(
  lpTimerAttributes: PSecurityAttributes; {!}
  bManualReset: BOOL;
  lpTimerName: PWideChar
): THandle; stdcall;


上面的四個系統內核對象(事件、互斥、信號、計時器)都是線程同步的手段, 從這也能看出處理線程同步的複雜性; 不過這還不是全部, Windows Vista 開始又增加了 Condition variables(條件變量)、Slim Reader-Writer Locks(讀寫鎖)等同步手段.

不過最簡單、最輕便(速度最快)的同步手段還是 CriticalSection(臨界區), 但它不屬於系統內核對象, 當然也就沒有句柄、沒有 TSecurityAttributes 這個安全屬性, 這也導致它不能跨進程使用; 不過寫多線程時一般不用跨進程啊, 所以 CriticalSection 應該是最常用的同步手段.
發佈了607 篇原創文章 · 獲贊 9 · 訪問量 32萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章