線程概念、線程創建、線程同步

1、前言

線程對於程序開發而言是一個很重要的概念,由於在實際的項目開發過程中經常會用到線程、多線程技術,所以就對線程的概念與使用進行一下簡單的總結,並對線程相關的概念如程序、進程、線程同步、線程池等概念也會進行相關的介紹。由於不同的環境、平臺會用到不同的線程開發技術,所以在本文章中也會對其他不同平臺的線程技術進行簡單的介紹。由於內容較多,所以具體更新時間不定。

2、概述

在使用線程技術之前,需要先介紹一下進程的概念,然後說明一下進程與線程的不同與聯繫,以免造成混淆。

2.1、進程

說到進程,我們可能首先會想到程序。因爲對於開發人員來說,程序是可見的,即是最終生成的可執行 *.exe 文件,而進程是看不見的。所以大部分人也就會認爲程序就是進程。這樣理解是不正確的。

程序是計算機指令的集合,它以文件的形式存儲在磁盤上,使我們經常見到的*.exe程序;而進程通常被定義爲一個正在運行的程序的實例,是一個程序在其自身的地址空間中的一次執行活動。由此也可以看出,程序與進程的不同。程序是“靜止”的,而進程是“活動”的,進程是在地址空間中執行程序指令集合的一次過程。

程序可以有多個進程。比如說,經常使用的記事本程序,我們能夠打開多個記事本文件,每一個記事本文件都是一個執行中的進程。

進程是資源申請、調度和獨立運行的單位,因此,它使用系統中的運行資源;而程序不能申請系統資源,不能被系統調度,也不能作爲獨立運行的單位,所以,程序不佔用系統的運行資源。

2.2、進程的組成

進程由兩部分組成:

(1)、操作系統用來管理進程的內核對象

內核對象是系統用於存儲關於進程的統計信息的地方。內核對象是操作系統內部分配的一個內存塊,用於存儲、維護該對象的各種信息。內核對象的數據結構只能被內核訪問並使用,所以只能通過Windows提供的一個系統函數對內核對象進行操作。

(2)、地址空間

它包含所有的可執行模塊或DLL模塊的代碼和數據。同時也包含動態內存分配的空間,例如線程的棧和堆的分配空間。

系統爲每一個進程都分配了獨立的虛擬地址空間。對於32位系統來說,它的尋址範圍爲2的32次方,即4GB,所以對於32位進程來說,分配的虛擬地址空間大小爲4GB。

2.3、進程與線程的關係

進程是不執行任何東西的,它只是線程的容器,進程是靠線程去執行操作的。若要使進程完成某項操作,它必須擁有一個在它的環境中運行的線程。此線程負責執行包含在進程地址空間中的代碼。也就是說,真正完成代碼執行的是線程,而進程只是線程的容器,或者說是線程的執行環境。

一個進程可以擁有多個線程,每個進程至少擁有一個線程。當創建一個進程時,操作系統會自動創建這個進程的第一個線程。當然我們可能感覺不到,因爲這個第一個線程被稱爲主線程,main()函數或者WinMain()函數可以被當爲該主線程的入口函數。然後就可在主線程中去完成其它線程的創建了。

3、線程

線程有兩部分組成:

(1)、線程的內核對象

操作系統通過線程的內核對象對線程實施管理,同時內核對象也是系統用來存放線程統計信息的地方。

(2)、線程棧

當創建一個線程時,系統會爲該線程分配一個線程內核對象。系統可已通過該內核對象對線程進行相關的操作。

線程總是在某個進程環境中創建的。系統從進程的地址空間中分配內存,供線程的棧使用。新線程運行的進程環境與創建線程的環境相同。因此,新線程可以訪問進程的內核對象的所有句柄、進程中的所有內存和在這個相同進程中的其他線程的堆棧。這使得單個進程中多個線程能夠非常容易的互相通信。

線程只有一個內核對象和一個棧,保留的記錄很少,因此需要的內存也很少。由於線程需要的開銷比進程少,因此在編程中進場採用多線程解決編程問題。

操作系統爲每一個運行線程安排一定的CPU時間---時間片。系統通過一種循環的方式爲線程提供時間片,線程在自己的時間片中運行,因爲時間片很短,因此就會造成多個線程同時進行的錯覺。如果在多CPU環境下,就會實現真正意義上的多線程同時運行。

3.1、線程的創建

3.1.1 CreatThread函數

在Windows環境下,線程的創建可以通過Windows API:CreatThread函數來完成的。CreatThread是Windows API,是最基礎的創建線程的函數

CreatThread函數聲明如下:

HANDLE CreateThread(
 LPSECURITY_ATTRIBUTES lpThreadAttributes,  // 線程的安全性屬性,可以設置爲NULL,使該線程使用默認安全屬性
 DWOED dwStaticSize,  //設置線程的初始棧大小,即線程可以將多少地址空間用於它自己的棧,已字節爲單位。可以設置爲0,表示默認使用與調用函數的線程相同的棧空間大小
 LPTHREAD_START_ROUTINE lpStartAddress, //指向應用程序定義的該類型函數的指針,該函數將由新線程執行,表明新線程的起始地址
 LPVOID lpRarameter, //通過該參數給創建的新線程傳遞參數
 DWOED dwCreationFlags, //用於設置控制線程創建的附加標記。可以將值設爲0,表示,線程創建之後就立即執行
 LPWORD lpThreadId  //該參數是一個返回值,用於接收線程ID,可以將值設爲NULL,表示對線程ID不感興趣
);

    其中,lpStartAddress表示爲該線程的入口函數。入口函數的定義需遵照如下的聲明形式:

DWORD WINAPI ThreadFun(LPVOID lpParameter);

該新線程入口函數有一個LPVOID類型的參數,並且返回值是DWORD類型。

線程創建實例如下所示:

DWORD WINAPI ThreadFun(LPVOID lp);
void main()
{
HANDLE hThread1;
hThread1 = CreateThread(NULL, 0, ThradFun, 0, NULL);
CloseHandle(hThread1);  //關閉線程
}
DWORD WINAPI ThreadFun(LPVOID lp)
{
***
return 0;
}

3.1.2 _beginthreadex函數

 _beginthreadex函數C語言運行庫函數,當在windows下使用C/C++進行編程時,優先使用該函數進行創建線程的操作。_beginthreadex函數原型如下所示,需包含頭文件<windows.h>,<process.h>:

uintptr_t _beginthreadex(
   void *security,//線程安全性,指定是否可以被繼承,默認爲NULL,表示不可繼承
   unsigned stack_size,//新線程的堆棧大小,默認爲0,含義與CreateThread函數一樣
   unsigned ( __stdcall *start_address )( void * ),//線程調用函數,unsigned __stdcall Func(void *) 類型的函數指針
   void *arglist,//參數列表
   unsigned initflag,//表示新線程的初始狀態,默認爲0,表示立即執行
   unsigned *thrdaddr//表示線程的ID
);

線程調用函數原型:

unsigned __stdcall ThreadFunc(void *pPara);

應用實例:

#include <Windows.h>
#include <process.h>
unsigned __stdcall thread_1_func(void *pNull)
{
	printf(">>> 啓動線程 1\n");
	printf("\n>>> 退出線程 1\n");
	return 0;
}

unsigned __stdcall thread_2_func(void *pNull)
{
	printf(">>> 啓動線程 2\n");
	printf("\n>>> 退出線程 2\n");
	return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
	HANDLE hThread_1, hThread_2;
	unsigned threadId_1, threadId_2;

	hThread_1 = (HANDLE)_beginthreadex(NULL, 0, &thread_1_func, NULL, 0, &threadId_1);
	hThread_2 = (HANDLE)_beginthreadex(NULL, 0, &thread_2_func, NULL, 0, &threadId_2);

	char ch = '0';
	while( ch != 'q' )
	{
		ch = getchar();
	}
	return 0;
}

3.1.3 AfxBeginThread函數

 AfxBeginThread函數是在當使用MFC編程時,MFC提供的一種創建線程的方式,在MFC環境下,優先考慮使用該函數

 MFC提供了兩種AfxBeginThread的重載函數定義,一種是用於用戶界面線程,一種是用於工作者線程,着重介紹工作者線程的創建。

AfxBeginThread函數原型如下所示:

CWinThread AfxBeginThread(
   AFX_THREADPROC pfnThreadProc, //線程的入口函數
   LPVOID pParam,        //傳遞如線程的參數,如果無參數,置爲NULL
   int nPriority,        //指定線程的優先級,如果爲0,表示與創建該線程的線程優先級相同
   UINT nStackSize,    //指定線程的堆棧大小,如果爲0,表示與創建該線程的線程優先級相同
   DWORD dwCreateFlags,//創建標識,默認爲0
   LPSECURITY_ATTRIBUTES lpSecurityAttrs //線程的安全屬性,NT下有用
);

線程入口函數定義:

UINT AFXThreadFunc(LPVOID pParam);

 應用實例:

CWinThread *thread = AfxBeginThread( AfxThreadFunc, NULL, 0, 0, 0, NULL);

if(thread == NULL)
{
    // 創建線程失敗!
}

UINT AfxThreadFunc(LPVOID pParam)
{
    // 進入線程入口函數
    return 0;
}

3.2、線程的同步

在多線程運行環境中,如果有多個線程要對同一塊內存地址進行操作,則必須要考慮線程同步的問題。線程同步說白了就是,保證在某一時刻只有一個線程去操作那同一塊內存地址。線程同步可以採用多種方式:關鍵段方式、內核對象方式。下main就簡單介紹一下同步的使用。

3.2.1 關鍵段(CriticalSection)

1、初始化關鍵段

CRITICAL_SECTION g_cs;
InitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION lp_cs, DWORD spin_count);

/* 之所以使用帶旋轉鎖的初始化方式,是因爲:當線程試圖進入一個關鍵段時,如果該關鍵段正在被另一個線程
佔用,函數會立即把調用線程切換到等待狀態。意味着線程必須從用戶模式切換到內核模式,而這種切換的開銷非
常大。爲了提高關鍵段的性能,把旋轉鎖合併到關鍵段中,當調用EnterCriticalSection時,他會用一個旋轉鎖
不斷的循環,會嘗試在一段時間內獲得對資源的訪問權。*/

該函數的第一個參數爲定義的關鍵段結構的地址,第二個參數爲希望旋轉的次數。可以默認設爲4000(用來保護進程堆的關鍵段使用的旋轉次數大約爲4000) 

2、進入關鍵段

EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);    //進入關鍵段

3、離開關鍵段

LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);    //離開關鍵段

4、銷燬關鍵段

DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);    //銷燬關鍵段

5、實例

#include <Windows.h>
#include <process.h>

CRITICAL_SECTION   g_cs;                // 定義關鍵段結構
int                g_count = 0;

unsigned __stdcall thread_1_func(void *pNull)
{
	printf(">>> 啓動線程 1\n");

    EnterCriticalSection(&g_cs);
    g_count ++;
    LeaveCriticalSection(&g_cs);

	printf("\n>>> 退出線程 1\n");
	return 0;
}

unsigned __stdcall thread_2_func(void *pNull)
{
	printf(">>> 啓動線程 2\n");

    EnterCriticalSection(&g_cs);
    g_count ++;
    LeaveCriticalSection(&g_cs);

	printf("\n>>> 退出線程 2\n");
	return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
	HANDLE hThread_1, hThread_2;
	unsigned threadId_1, threadId_2;

    InitializeCriticalSectionAndSpinCount(&g_cs, spin_count);

	hThread_1 = (HANDLE)_beginthreadex(NULL, 0, &thread_1_func, NULL, 0, &threadId_1);
	hThread_2 = (HANDLE)_beginthreadex(NULL, 0, &thread_2_func, NULL, 0, &threadId_2);

    DeleteCriticalSection(&g_cs);

	return 0;
}

3.2.2 互斥量(Mutex)

1、創建互斥量

HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lp_ma, BOOL bInitialOwner, LPCSTR lpName);

2、等待互斥量,以獲取使用權

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);

3、釋放互斥量,放棄使用權

BOOL ReleaseMutex(HANDLE hMutex);

4、銷燬互斥量

CloseHandle(HANDLE hHandle);

5、實例

#include <Windows.h>
#include <process.h>

HANDLE             g_hmu;                // 定義關鍵段結構
int                g_count = 0;

unsigned __stdcall thread_1_func(void *pNull)
{
	printf(">>> 啓動線程 1\n");

    WaitForSingleObject(g_hmu, INFINITE);
    g_count ++;
    ReleaseMutex(g_hmu);

	printf("\n>>> 退出線程 1\n");
	return 0;
}

unsigned __stdcall thread_2_func(void *pNull)
{
	printf(">>> 啓動線程 2\n");

    WaitForSingleObject(g_hmu, INFINITE);
    g_count ++;
    ReleaseMutex(g_hmu);

	printf("\n>>> 退出線程 2\n");
	return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
	HANDLE hThread_1, hThread_2;
	unsigned threadId_1, threadId_2;

    g_hmu = CreateMutex(NULL, FALSE, NULL);

	hThread_1 = (HANDLE)_beginthreadex(NULL, 0, &thread_1_func, NULL, 0, &threadId_1);
	hThread_2 = (HANDLE)_beginthreadex(NULL, 0, &thread_2_func, NULL, 0, &threadId_2);

    CloseHandle(g_hmu);

	return 0;
}

3.2.3 關鍵段與互斥量比較

 

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