C/C++ Muti-Thread多線程編程學習(之)線程Thread | 創建、運行、結束

前言


  多線程(Multi-Thread),是指從軟件或者硬件上實現多個線程併發執行的技術。無論你是軟件開發工程師(Software Engineer),還是算法工程師(Algorithm Engineer),當遇到性能優化需求時,多線程技術是不可繞開的一項。

  現代處理器是多核處理器架構,單處理器有多個獨立運算核,可以併發運算,這個特性使得總任務可以被劃分爲多個獨立的子任務同時運行,大大提高運行效率。本系列將對c/c++多線程編程中運用最廣泛的概念做一個入門介紹,希望能對讀者學習多線程編程有所幫助。

  本文要介紹的是最基礎的那個概念:線程Thread

線程 Thread


  我們先看看Wiki對Thread的介紹:

  In computer science, a thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler.
  Multiple threads can exist within one process, executing concurrently and sharing resources such as memory, while different processes do not share these resources.

  在計算機科學中,執行線程是能被調度器獨立管理的程序化指令中的最小序列。
  多個線程可存在與一個進程中,同時執行且共享資源(如內存資源),而不同的進程則不能共享這些資源。

  從以上介紹中,我們提煉出兩個關鍵點:線程之間彼此獨立、共享資源。這表明線程不僅可以獨立併發,而且可以通信協作。也就是說,多線程不是各自爲戰,而是同舟共濟。

  在多線程編程中,線程是最基本的概念。面對一個任務並行的多線程問題,我們首先要將總任務分爲多個子任務,一個子任務就可以放入一個線程中執行,這時我們需要編寫多個線程,每個線程負責獨立的任務;而面對一個數據並行的多線程問題,我們只有一個任務,是將數據分爲多個獨立的子數據,每個數據都執行同樣的任務,這時我們只需要編寫一個線程,通過管理線程的輸入來完成多個子數據的並行任務。所以我們把線程想象成任務即可,這個任務要完成多少工作無關緊要,只要多個任務是獨立的,或者同一個任務有多個獨立的輸入。

創建線程

CreateThread

  在C語言中,如何創建一個線程?在Windows系統中,我們可以使用CreateThread函數:

HANDLE WINAPI CreateThread(
	_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,		
	_In_ SIZE_T dwStackSize,									
	_In_ LPTHREAD_START_ROUTINE lpStartAddress,				
	_In_opt_ __drv_aliasesMem LPVOID lpParameter,			
	_In_ DWORD dwCreationFlags,
	_Out_opt_ LPDWORD lpThreadId
);

  參數說明:
  lpThreadAttributes:指向SECURITY_ATTRIBUTES型態的結構的指針。在Windows 98中忽略該參數。在Windows NT中,NULL使用默認安全性,不可以被子線程繼承,否則需要定義一個結構體將它的bInheritHandle成員初始化爲TRUE
  dwStackSize:設置初始棧的大小,以字節爲單位,如果爲0,那麼默認將使用與調用該函數的線程相同的棧空間大小。任何情況下,Windows根據需要動態延長堆棧的大小。
  lpStartAddress:指向線程函數的指針,形式:函數名,函數名稱沒有限制,但是必須以下列形式聲明:

DWORD WINAPI 函數名 (LPVOID lpParam) 

  格式不正確將無法調用成功。
  lpParameter:向線程函數傳遞的參數,是一個指向結構的指針,不需傳遞參數時,爲NULL。
  dwCreationFlags :線程標誌,可取值如下
  (1)CREATE_SUSPENDED(0x00000004):創建一個掛起的線程,
  (2)0:表示創建後立即激活。
  (3)STACK_SIZE_PARAM_IS_A_RESERVATION(0x00010000):dwStackSize參數指定初始的保留堆棧 的大小,否則,dwStackSize指定提交的大小。該標記值在Windows 2000/NT and Windows Me/98/95上不支持。
  lpThreadId:保存新線程的id。若不想返回線程ID,設置值爲NULL。
  返回值:函數成功,返回線程句柄;函數失敗返回false。

  代碼示例:

#include "stdafx.h"
#include "windows.h"

//線程函數定義
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
	Sleep(1000);
	return 0;
}
int main()
{
	DWORD dwThreadId = 0;	//線程ID
	//創建線程
	HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &dwThreadId);

    return 0;
}

_beginthread

  實際上,CreateThread函數並不被推薦使用,當軟件需要調用CRT庫時,使用CreateThread因爲沒有對子線程爲CRT庫分配堆,會導致內存錯誤而崩潰。

  我們可以使用另一個創建線程函數:_beginthread():

uintptr_t _beginthread(
void( *start_address )( void * ),
unsigned stack_size,
void *arglist
);

  參數說明:
  start_address:新線程的起始地址 ,指向新線程調用的函數的起始地址,爲函數名。函數的聲明方式很簡單:

void 函數名(LPVOID lpParam)

  stack_size:新線程的堆棧大小,一般爲0
  arglist:傳遞給線程的參數列表,無參數時爲NULL

  _beginthread要比CreateThread安全,它是對CreateThread函數的封裝,並針對CreateThread在調用CRT庫時的內存泄漏問題進行了處理,所以比CreateThread更加安全。_beginthrad對應的關閉線程函數爲爲_endthread。

  代碼示例:

#include "stdafx.h"
#include "windows.h"
#include "process.h"

//線程函數定義
void ThreadFunc(void* param)
{
	Sleep(1000);
}

int main()
{
	//創建線程
	_beginthread(ThreadFunc, 0, NULL);
   
	return 0;
}

_beginthreadex

  _beginthread函數非常簡單,易上手,但參數太少也有缺點,和CreateThread比起來似乎可控制性不強?別慌,我們還有另一個函數_beginthreadex:

unsigned long _beginthreadex(
    void *security,
    unsigned stack_size,  
    unsigned(_stdcall *start_address)(void *), 
    void *argilist, 
    unsigned initflag,  
    unsigned *threaddr 
);

  參數說明:
  security:安全屬性, 爲NULL時表示默認安全性
  stack_size:線程的堆棧大小, 一般默認爲0
  start_address:所要啓動的線程函數
  argilist:線程函數的參數, 是一個void*類型, 傳遞多個參數時用結構體
  flag:新線程的初始狀態,0表示立即執行,CREATE_SUSPEND表示創建之後掛起
  threaddr:線程ID地址
  返回值:成功返回新線程句柄, 失敗返回0

  可以看到,_beginthreadex就像_beginthread的擴展體,增加了三個參數,是可控制性更好一些。兩個函數的不同點如下:
  (1)_beginthreadex()比_beginthread()多3個參數:intiflag,security和threadaddr。
  (2)線程函數的聲明方式不同。_beginthreadex()的線程函數必須使用_stdcall修飾符,且必須返回一個unsigned int型的退出碼。

unsigned __stdcall 函數名(void* param)

  (3)_beginthreadex()在創建線程失敗時返回0,而_beginthread()在創建線程失敗時返回-1。這一點是在檢查返回結果時必須注意的。
  (4)如果是調用_beginthread創建線程,則相應地調用_endthread結束線程時,系統會自動關閉線程句柄;而調用_beginthreadx創建線程,相應地調用_endthreadx結束線程時,系統不能自動關閉線程句柄。因此調用_beginthreadx創建線程還需程序員自己關閉線程句柄,以清除線程的地址空間。

  _beginthread和_beginthreadex都包含於頭文件 process.h 中。
  代碼示例:

#include "stdafx.h"
#include "windows.h"
#include "process.h"

//線程函數定義
unsigned __stdcall ThreadFunc(void* param)
{
	Sleep(1000);
	return 0;
}

int main()
{
	unsigned uiThreadId = 0;
	//創建線程
	HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uiThreadId);
   
	return 0;
}

pthread_create

  pthread_create是類Unix操作系統(Unix、Linux、Mac OS X等)的創建線程的函數。包含於頭文件pthread.h中。

int pthread_create(
	pthread_t *tidp,
	const pthread_attr_t *attr,
	(void*)(*start_rtn)(void*),
	void *arg
);

  參數說明:
  tidp:第一個參數爲指向線程標識符的指針。
  attr:用來設置線程屬性。
  start_rtn:線程運行函數的起始地址。
  arg:運行函數的參數。
  若線程創建成功,則返回0。若線程創建失敗,則返回出錯編號,並且*thread中的內容是未定義的。
  返回成功時,由tidp指向的內存單元被設置爲新創建線程的線程ID。attr參數用於指定各種不同的線程屬性。新創建的線程從start_rtn函數的地址開始運行,該函數只有一個萬能指針參數arg,如果需要向start_rtn函數傳遞的參數不止一個,那麼需要把這些參數放到一個結構中,然後把這個結構的地址作爲arg的參數傳入。

線程運行

  一般來說,線程創建之後,會自動運行,如使用_beginthread創建的線程,但是當設置了線程啓動狀態時,可以控制線程的啓動時刻,使用CreateThread和_beginthreadex創建線程才能完成這個操作,將creation flag設置爲CREATE_SUSPENDED,這時線程創建後將被掛起,直到調用ResumeThread函數激活線程。
  代碼示例:

#include "stdafx.h"
#include "windows.h"
#include "process.h"

//線程函數定義
unsigned __stdcall ThreadFunc(void* param)
{
	Sleep(1000);
	return 0;
}

int main()
{
	unsigned uiThreadId = 0;	//線程ID
	//創建線程並掛起
	HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, NULL, CREATE_SUSPENDED, &uiThreadId);
   //激活線程
	ResumeThread(hThread);

	return 0;
}

  在線程運行過程中,我們往往要等待線程運行結束再往下執行。可以使用WaitForSingleObject來執行這一工作。

DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);

  參數說明:
  hHandle:線程句柄
  dwMilliseconds:等待時間,單位爲milliseconds(毫秒)。如果指定一個非零值,函數處於等待狀態直到hHandle標記的對象被觸發,或者時間到了。如果dwMilliseconds爲0,對象沒有被觸發信號,函數不會進入一個等待狀態,它總是立即返回。如果dwMilliseconds爲INFINITE,對象被觸發信號後,函數纔會返回。
  代碼示例:

#include "stdafx.h"
#include "windows.h"
#include "process.h"

//線程函數定義
unsigned __stdcall ThreadFunc(void* param)
{
	Sleep(1000);
	return 0;
}

int main()
{
	unsigned uiThreadId = 0;	//線程ID
	//創建線程並掛起
	HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, NULL, CREATE_SUSPENDED, &uiThreadId);
   //激活線程
	ResumeThread(hThread);

	WaitForSingleObject(hThread, INFINITE);
	return 0;
}

結束線程

  每個創建線程函數,都會有着對應的結束線程函數,CreateThread對應ExitThread,_beginthread對應_endthread,_beginthreadex對應_endthreadex,可以在必須強制結束線程的時候調用它們,但是最好避免這樣做,因爲中途強制結束,會導致不可控制的資源泄漏。一般而言,等待線程函數自行return是最好的方式,這時系統會自動調用線程終止函數,並釋放所有資源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章