Windows多線程編程入門


標簽(空格分隔): Windows multithread programming 多線程 並發 編程


背景知識

在開始學習多線程編程之前,先來學習下進程線程

進程

進程是指具有一定獨立功能的程序在某個數據集合上的一次運行活動,是系統進行資源分配和調度運行的一個基本單位。簡單地說,晉城市程序在計算機上的一次執行活動,當你啟動了一個程序,你就啟動了一個進程,退出一個程序,也就結束了一個進程。
打開windows任務管理器-->詳細信息,可以看到Windows系統下有很多進程在運行。

注意:
程序並不等於進程。程序只是一組指令的有序集合,它本身沒有任何運行的含義,只是一個靜態實體。進程是程序在某個數據集上的執行,是一個動態實體。

程序只有被裝入內存后才能運行,程序一旦進入到內存就成為進城了,因此,進程的創建過程也就是程序從外存儲器(硬盤或者網卡)被加載到內存的過程。進程因創建而產生,因調度而運行,因等待資源或事件而處於等待狀態,因完成任務而銷毀,它反映了一個程序在一定的數據集上運行的全部動態過程。

進程在其存在過程中,由於多個進程的並發執行,受到CPU、外部設備等資源的制約,使得它們的狀態不斷發生變化。進程的基本狀態有三種:就緒狀態、運行狀態、阻塞狀態。三種狀態可以相互轉化。

  • 就緒狀態:進程獲得除了CPU之外的一切運行所需的資源,等待獲得CPU,一旦獲得CPU即可立即運行。(如數據已經准備好,或者接收到有新數據,需要CPU來處理)
  • 運行狀態:進程獲得了包括CPU在內的一切資源,正在CPU上運行。(正在運行指令,處理數據)
  • 阻塞狀態:正在CPU上運行的進程,由於某種原因,不再具備運行的條件
    而暫時停止運行。(比如需要等待I/O完成、當前進程的CPU時間片耗盡、等待其他進程發來消息、等待用戶完成輸入等)。

進程調度:當就緒進程的數目多於CPU的數目時,需要按照一定的算法動態地將CPU分配給就緒進程隊列中的某一個進程,並使之運行,這就是所謂的進程調度。

當分配給某個進程的運行時間(時間片)用完了時,進程就會有運行狀態回到就緒狀態。運行中的進程如果需要執行I/O操作,比如從鍵盤輸入數據,就會進入到阻塞狀態等待I/O操作完成,I/O操作完成后,就會轉入就緒狀態等待下一次調度。

進程調度的關鍵是進程調度算法。進程調度算法解決兩個問題:一是當CPU空閑時,選擇哪個就緒進程運行;二是進程占有CPU后,它能運行多長時間。后一個問題也稱為調度方式。調度方式有兩種:不可搶占(或不可剝奪)方式和可搶占(或可剝奪)方式。
不可搶占方式是指一旦某個就緒進程獲得CPU后,只要不是進程主動放棄,將會一直運行下去,直到運行結束,期間CPU不可剝奪。可搶占方式是指:當一個進程正在運行時,系統可根據某種原則,剝奪其CPU的使用並分配給其他進程,剝奪原則包括優先權、短進程優先、時間片等。

進程調度算法種類較多,但概括起來最基本的算法主要有靜態優先級,動態優先級和時間片輪轉等。實際系統中采用的調度算法一般是多種算法結合和改進,Windows系統采用的是“搶占式多任務”就是一種時間片和優先級相結合的調度方式。系統為每個進程分配一定的CPU時間,當程序的運行超過規定時間后,系統就會中斷該進程並把CPU的控制權轉交給優先級較高的進程,如果無更高級別的進程,則轉交給其他相同優先級的進程。


線程

線程是為了在進程內部實現並發性而引入的概念。
進程內部的並發行是指:在同一個進程內部可以同時進行多項工作,而線程就是完成其中某一項工作的單一指令序列。一般情況下,同一進程中的多個線程各自完成不同的工作,比如一個線程負責通過網絡收發數據,另一個線程完成所需的計算工作,第三個線程來執行文件輸入輸出,當其中一個由於某種原因阻塞后,比如通過網絡收發數據的線程等待對方發送數據,另外的線程仍然能執行而不會被阻塞。
一個進程內的所有線程使用同一個地址空間,即各線程是在同一地址空間運行的,各線程自己並不獨立擁有系統資源,但它可與同屬一個進程的其他線程共享進程所擁有的全部資源。解決同意進程的各線程之間如何共享內存、如何通信等問題是多線程編程中的難點。由於線程之間的相互制約,以及程序功能的需求,線程在運行中也會呈現出間斷性,因此一個線程在其生命期內有兩種存在狀態--運行狀態和阻塞(掛起)狀態。有很多原因可導致線程在這兩種狀態之間進行切換。線程的狀態相互轉換常見的如Sleep()函數的調用, Suspend(),等待鎖可用,等待I/O操作,Resume(),I/O操作完成,

線程僅僅簡單地借用了進程切換的概念,它把進程間的切換變成了同一個進程內的幾個函數間的切換。同一個進程中函數間的切換相對於進程切換來說所需的開銷要小得多,它只需要保存少數幾個寄存器、一個堆棧指針以及程序計數器等少量內容。在進程內創建、終止線程比操作系統創建、終止進程也要快。
有多個線程的程序稱為多線程程序。Windows系統支持多線程程序,允許程序中存在多個線程。事實上,任何一個Windows中的應用程序都至少有一個線程,即主線程,其他線程都是主線程的子孫線程。


進程與線程的區別

進程與線程的主要區別在於,多進程中每個進程都有自己的地址空間(address space),而多線程則共享同一進程的地址空間;進程是除CPU以外的資源分配的基本單位,線程主要是執行和調度(CPU運行時間分配)的基本單位。
線程是進程內部的一個執行單元。每一個進程至少有一個主執行線程,它無須用戶去主動創建,是系統自動創建的。用戶根據需要在應用程序中創建其他線程,多個線程並發地運行於同一個進程中。
一個進程中的所有線程都在該進程的虛擬地址空間中,共同使用該虛擬地址空間中的全局變量和系統資源,所以線程間的通信非常方便。
系統創建好進程后,實際上就啟動了執行該進程的主執行線程。在VC++程序中,主線程的啟動點是以函數形式(即main或WinMain函數)提供給Windows系統。主線程終止了,進程也將隨之終止,而不管其他線程是否執行完畢。
多線程可以實現並行處理,避免了某項任務長時間占用CPU時間。當線程數目多於計算機的CPU數目時,為了運行所有這些線程,操作系統為每個獨立線程安排一些CPU時間,操作系統以輪換方式向線程提供時間片,這就造成了一種假象:好像這些線程都在同時運行。
盡管比進程間的切換要好得多,但是線程間的切換仍會消耗很多的CPU資源,在一定程度上,也會降低系統的性能。


Windows多線程編程

多線程給應用開發帶來了許多好處,但並非在任何情況下都要使用多線程,一定要根據應用程序的具體情況來綜合考慮。一般來說,以下情況下可以考慮使用多線程:

  • 應用程序中的各任務相對獨立
  • 某些任務耗時較多
  • 各任務需要有不同的優先級

在VC++程序設計中,有多種方法在程序中實現多線程

  1. Win32SDK函數
  2. 使用C/C++運行時庫函數
  3. 使用MFC類庫
使用Win32 SDK函數實現多線程
1.創建線程

在程序中創建一個線程需要以下兩個步驟:

  1. 編寫線程函數
    所有線程必須從一個指定的函數開始執行,該函數就是所謂的線程函數。線程函數必須具有類似下面所示的函數原型:
DWORD ThreadFunc(LPVOID lpvThreadParam);

ThreadFunc是新創建的線程函數的名字,可以由編程者任意指定,但必須符合VC++標識符的命名規范。該函數僅有一個LPVOID類型的參數,LPVOID的類型定義如下:

typedef void * LPVOID;

它既可以是一個DWORD類型的整數,也可以是一個指向一個緩沖區的void類型的指針。函數返回一個DWORD類型的值。
一般來說,C++的類成員函數不能作為線程函數。這是因為在類中定義的成員函數,編譯器會給其加上this指針。但如果需要線程函數像類的成員函數那樣能訪問類的所有成員,可采用兩種方法。第一種方法是將該成員函數聲明為static類型,但static成員函數只能訪問static成員,不能訪問類中的非靜態成員,解決此問題的一種途徑是可以在調用類靜態成員函數(線程函數)時,將this指針作為參數傳入,並在該線程函數中用強制類型轉換將this轉換成指向該類的指針,通過該指針訪問非靜態成員。第二種是不定義類成員函數為線程函數,而將線程函數定義為類的友元函數,這樣線程函數也可以有淚成員函數同等的權限。

  1. 創建一個線程
    進程的主線程是操作系統在創建進程時自動生成的,但如果要讓一個線程創建一個新的線程,則必須調用線程創建函數。Win32 SDK提供的線程創建函數是CreateThread()。

函數原型

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

函數參數

  • lpThreadAttributes:指向一個SECURITY_ATTRIBUTES結構的指針,該結構決定了線程的安全屬性,一般置為NULL
  • dwStackSize:指定線程的堆棧深度,一般設置為0
  • lpStartAddress:線程起始地址,通常為線程函數名
  • LPTHREAD_START_ROUTINE類型定義:
typedef unsigned long (__stdcall *LPTHREAD_START_ROUTINE) (void* lpThreadParameter);
  • lpParameter: 線程函數的參數
  • dwCreationFlags: 控制線程創建的附加標志。該參數為0,則線程在被創建后立即開始執行;如果該參數為CREATE_SUSPENDED, 則創建線程后蓋線程處於掛起狀態,直至函數ResumeThread被調用
  • lpThreadID: 該參數返回所創建線程的ID

返回值

  • 該函數在其調用進程的進程空間里創建一個新的線程,並返回已建線程的句柄,如果創建成功則返回線程的句柄,否則返回NULL。
    注意:使用同一個線程函數可以創建多個各自獨立工作的線程。

編寫如下示例程序1.

// TestProject.cpp : 定義控制台應用程序的入口點。
#include "stdafx.h"
#include <stdio.h>
#include <Windows.h>
#define N 100000

int ThreadF0(LPVOID lpParam)
{
	long *a = (long*)lpParam;
	for (int i = 0; i < N; ++i) {
		//InterlockedExchangeAdd(a, 1);
		(*a) +=1;
		//Sleep(3);
		//printf("0: %d\n", *a);
	}
	printf("0: %d\n", *a);
	return 0;
}

int ThreadF2(LPVOID lpParam)
{
	long *b = (long*)lpParam;
	for (int i = 0; i < N; ++i) {
		(*b) += 1;
		//InterlockedExchangeAdd(b, 1);
		//Sleep(2);
		//printf("   1: %d\n", *b);
	}
	printf("2: %d\n", *b);
	return 0;
}

int main(int argc, char *argv[])
{
	int t = 0;
	HANDLE htd0, htd1, htd2;
	DWORD thrdID0, thrdID2;
	htd0 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadF0, (void*)&t, 0, &thrdID0);
	htd1 = 0;
	//htd1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadF1, (void*)&t, 0, &thrdID1);
	htd2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadF2, (void*)&t, 0, &thrdID2);
	Sleep(1000);
	printf("t:%d\n", t);
	printf("hello, world\n");
	return 0;
	}

輸出如下:

0: 100000
2: 120107
t:120107
hello, world
請按任意鍵繼續. . .

需要特別說明的是:
這里的循環次數N不能設置得太小,因為現在CPU運行速度很快,如果設置得太小是無法(理論上也可以看到,只是出現的概率很低)看到示例1中線程切換引起的異常的。

如果沒有線程切換,t最終的值應該是200000,但是這里線程0和線程2切換,彼此相互影響了,使得t最后沒有達到200000.

另,sleep會引起線程之間的主動切換。所以,系統會每次在運算(加1之前或者加1之后)后,如果你在線程中使用了sleep遇到sleep就切換線程。

所以需要在正在加1的過程中切換線程,才能看到這樣不做控制是有問題的。
而要在正在加的過程中切換線程,只能由系統自動切,不能通過主動調用sleep來實現。

在簡單的加法操作中,可以使用InterlockedExchangeAdd函數來進行運算,這樣就不會怕線程切換了。
復雜的多步操作用鎖、臨界區等線程同步的操作。

而操作系統的進程(或者線程)間通信,可以用事件對象(Windows系統)結合WaitForSingleObject()函數,socket,信號,管道,消息隊列,共享內存等方式

另外兩個小的線程例子參見:

此外,Windows操作系統還提供了Sleep()函數

VOID Sleep(DWORD dwMilliseconds);

Sleep()函數是一個Windows API函數,其功能使線程阻塞dwMilliseconds毫秒。使用Sleep()函數需要包含頭文件"windows.h"

函數參數:
dwMilliseconds:指定線程阻塞的時間長度,時間的單位是毫秒(ms)。如果參數取值為0,執行該函數也將使線程阻塞轉而執行其他同優先級的線程,如果不存在其他同優先級的線程,線程將立刻恢復執行。如果取值為常量INFINITE,則線程將被無限期阻塞。

線程函數的參數傳遞
由CreateThread函數原型可以看出,創建線程時,可以給線程傳遞一個void指針類型的參數,該參數為CreateThread()函數的第四個參數。
當需要將一個整型數據作為線程函數的參數傳遞給線程時,可將該整型數據強制轉換為LPVOID類型,作為它的實參傳遞給線程函數。
當需要向線程傳遞一個字符串時,則創建線程時的實參傳遞既可以使用字符數組,也可以使用std::string類。使用字符數組時,實參可直接使用字符數組名或指向字符串的char *類型的指針。使用std::string類型時,可將指向std::string對象的指針強制轉換為LPVOID。
如果需要向線程傳送多個數值時,由於線程函數的參數只有一個,所以需要先將它們封裝在一個結構體變量中,然后將該變量的指針作為參數傳遞給線程函數。

示例代碼見:
https://github.com/xiaoliuzi/netlib_demo/tree/master/learn_multi_thread


參考:
《Windows網絡編程基礎教程》 楊傳棟 張煥遠 編著 清華大學出版社


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM