Windows多線程問題

進程和線程是操作系統裏面經常遇到的兩個概念,還有一個概念,是應用程序。應用程序包括指令和數據,在開始運行之前,只是分佈在磁盤上的指令和數據。正在執行的應用程序稱爲進程,進程不僅僅是指令和數據,它還有狀態狀態是保存在處理器寄存器中的一些值,記錄一些信息,比如說當前執行指令的地址,保存在內存中的值等。進程是應用程序的基本構件塊,同時運行的多個應用程序就是多個進程。每個進程可以運行多個線程。線程也有一些狀態,但線程的狀態基本上只是保存在其寄存器中的值以及其棧上的數據。線程與同一個應用程序中的其他線程共享很多狀態。進程的優點是每個進程是獨立的,一個進程的死掉對其他正在運行的進程沒有任何影響,多進程的缺點是每個進程都需要自己的TLB(Translation Look-aside Buffer,轉換旁視緩衝器)條目,從而增加了TLB條目和緩存的未命中率,多進程還有一個缺點,就是進程之間共享數據需要顯式控制,這種操作的開銷比較大。多線程的有點事多線程之間共享數據的成本低,因爲某個線程可以將數據項存儲到內存,且該數據立刻對此進程中的所有其他線程可見,另外一個優點是所有線程共享相同的TLB和緩存條目,所以多線程應用程序的緩存未命中率較低。缺點是一個線程失敗就很有可能導致整個應用程序終止。比如說,瀏覽器是多進程的,可以使用瀏覽器打開多個標籤頁,每個標籤頁是一個單獨的進程,一個標籤頁的失敗不會導致整個瀏覽器的崩潰。如果瀏覽器做成多線程的,如果一個線程執行一些bad code,整個瀏覽器很可能會崩掉。

要使多線程應用程序有效地工作,必須在線程之間共享某些共有狀態。當多線程以不安全得方式更新同一數據就會產生數據爭用,避免數據爭用的一個方法就是正確使用線程同步。同步原語包括互斥量和臨界區、自旋鎖、信號量、讀寫鎖、屏障。對於線程和進程間通信,有很多機制,比如說內存、共享內存和內存映射文件、條件變量、信號和事件、消息隊列、命名管道、網絡棧等。

創建線程

Windows操作系統對多線程的支持大體上與POSIX線程提供的支持類似。創建Windows的本機線程可以調用CreateThread函數,該函數的返回值爲所創建的線程的句柄,如果返回值爲0,則說明調用不成功。

#include <Windows.h>
#include <stdio.h>
#include <tchar.h>
DWORD WINAPI mythread(__in LPVOID lpParameter)
{
	printf("Thread %i\n",GetCurrentThread());
	return 0;
}
int _tmain(int argc,_TCHAR* argv[]){
	HANDLE handle;
	handle=CreateThread(0,0,mythread,0,0,0);
	getchar();
	return 0;
}

在上面的一段代碼中,調用CreateThread會令操作系統生成一個新線程,然後返回該線程的句柄,但是運行庫並沒有建立其所需要的線程本地數據結構。運行庫也提供了兩個線程創建函數_beginthread()和_beginthreadex()。這兩者的區別是_beginthread()創建的線程在線程退出時會關閉線程句柄,_beginthreadex()調用返回的線程句柄則需要顯式調用CloseHandle才能釋放。

#include <Windows.h>
#include <process.h>
#include <stdio.h>
#include <tchar.h>
DWORD WINAPI mythread1(__in LPVOID lpParameter)
{
	printf("CreateThread創建的線程,ID爲:%i\n",GetCurrentThreadId());
	return 0;
}
unsigned int __stdcall mythread2(void *data)
{
	printf("__beginthreadex創建的線程,ID爲:%i\n",GetCurrentThreadId());
	return 0;
}
void mythread3(void *data)
{
	printf("__beginthread創建的線程,ID爲:%i\n",GetCurrentThreadId());
}
int _tmain(int argc,_TCHAR* argv[])
{
	HANDLE h1,h2,h3;
	h1=CreateThread(0,0,mythread1,0,0,0);
	h2=(HANDLE)_beginthreadex(0,0,&mythread2,0,0,0);
	WaitForSingleObject(h2,INFINITE);
	CloseHandle(h2);

	h3=(HANDLE)_beginthread(&mythread3,0,0);
	getchar();
}

在多線程下面,有時希望等待某一線程完成了再繼續做其他事情,要實現這個目的,可以使用Windows API函數WaitForSingleObject,或者WaitForMultipleObjects。這兩個函數都會等待Object被標爲有信號(signaled)時才返回。

在Windows操作系統中,經常提及一個概念,句柄,而且許多Windows API的返回值都是句柄,其實句柄說白了就是一個無符號整數。返回句柄的Windows API調用實際上是在內核空間創建某個資源,句柄只是這個資源的索引。當應用程序使用完該資源後,就可以調用CloseHandle函數讓內核釋放相關的內核空間的資源。

終止線程可以調用ExitThread或者TerminateThread,也可以調用庫函數endthread或者endthreadex來終止線程。

如果線程處於掛起狀態,則啓動線程可以調用ResumeThread函數,該函數以線程的句柄爲參數。SuspendThread函數可以迫使運行中的線程掛起,這個函數最好不要輕易調用,因爲如果掛起線程時,線程正好持有互斥變量等這些資源,就很容易出問題,呵呵。

#include <Windows.h>
#include <process.h>
#include <stdio.h>
#include <tchar.h>
unsigned int __stdcall mythread(void *data)
{
	printf("創建的線程ID爲:%i\n",GetCurrentThreadId());
	return 0;
}
int _tmain(int argc,_TCHAR* argv[])
{
	HANDLE h;
	h=(HANDLE)_beginthreadex(0,0,&mythread,0,CREATE_SUSPENDED,0);
	getchar();
	ResumeThread(h);
	getchar();
	WaitForSingleObject(h,INFINITE);
	CloseHandle(h);
	return 0;
}

線程同步和資源共享

Windows提供的同步對象與POSIX規定的很相似。線程的同步還是那種方式,比如說互斥鎖、臨界區、讀寫鎖、信號量、條件變量、事件等。

給個例子。

#include <Windows.h>
#include <process.h>
#include <stdio.h>
#include <tchar.h>
#include <math.h>
int isPrime(int num)
{
	int i;
	for (i=2;i<(int)(sqrt((float)num)+1.0);i++)
	{
		if (num%i==0)
			return 0;
	}
	return 1;
}
volatile int counter=2;
unsigned int __stdcall test(void *)
{
	while (counter<20)
	{
		int num=counter++;
		printf("Thread ID : %i; value = %i, is prime = %i\n",GetCurrentThreadId(),num,isPrime(num));
	}
	return 0;
}
int _tmain(int argc,_TCHAR* argv[])
{
	HANDLE h1,h2;
	h1=(HANDLE)_beginthreadex(0,0,&test,(void *)0,0,0);
	h2=(HANDLE)_beginthreadex(0,0,&test,(void *)1,0,0);
	WaitForSingleObject(h1,INFINITE);
	WaitForSingleObject(h2,INFINITE);
	CloseHandle(h1);
	CloseHandle(h2);
	getchar();
	return 0;
}



用兩個線程計算某個給定範圍內的所有素數。創建兩個線程,這兩個線程會一直測試數字,直到2-20以內的所有數字計算完畢爲止。從執行結果可以看出,系統共產生兩個線程,ID分別爲13788和13792,這兩個線程會同時訪問共享變量counter,肯定會導致數據爭用這個問題,如果希望每個線程測試不同的數字,有必要採取一定的措施對共享變量counter的操作進行保護。顯示順序的不同是因爲線程完成對一個數字的判斷所用的時間與函數調用printf輸出顯示的時間有差距。

解決的方案有很多,無非就是解決線程同步的經典措施。

第一種方法,可以添加對臨界區代碼的訪問保護,確保僅有單個線程執行。臨界區的聲明可以調用InitializeCriticalSection(),調用DeleteCriticalSection()刪除臨界區,如果線程希望進入臨界區,可以調用EnterCriticalSection()函數,如果此時臨界區中午其他線程,調用線程就能進入臨界區並執行相關的代碼;如果臨界區有線程,則調用線程將休眠,直到正在執行臨界區的線程調用LeaveCriticalSection()離開臨界區。調用EnterCriticalSection()的線程會直到獲得臨界區的訪問權纔會離開,沒有什麼超時不超時的概念,呵呵。

#include <Windows.h>
#include <process.h>
#include <stdio.h>
#include <tchar.h>
#include <math.h>
int isPrime(int num)
{
	int i;
	for (i=2;i<(int)(sqrt((float)num)+1.0);i++)
	{
		if (num%i==0)
			return 0;
	}
	return 1;
}
volatile int counter=2;
CRITICAL_SECTION critical;
unsigned int __stdcall test(void *)
{
	while (counter<20)
	{
		EnterCriticalSection(&critical);
		int num=counter++;
		LeaveCriticalSection(&critical);
		printf("Thread ID : %i; value = %i, is prime = %i\n",GetCurrentThreadId(),num,isPrime(num));
	}
	return 0;
}
int _tmain(int argc,_TCHAR* argv[])
{
	HANDLE h1,h2;
	InitializeCriticalSection(&critical);
	h1=(HANDLE)_beginthreadex(0,0,&test,(void *)0,0,0);
	h2=(HANDLE)_beginthreadex(0,0,&test,(void *)1,0,0);
	WaitForSingleObject(h1,INFINITE);
	WaitForSingleObject(h2,INFINITE);
	CloseHandle(h1);
	CloseHandle(h2);
	getchar();
	DeleteCriticalSection(&critical);
	return 0;
}

實際上,使線程休眠後再喚醒線程比較耗時,因爲這涉及到進入內核。因爲很可能線程進入休眠時,原來已經處於臨界區的線程已經離開了,此時讓等待的線程休眠後再喚醒就有點扯淡了。。。可以調用TryEnterCriticalSection()立即返回,返回值爲真時表示該線程獲得對臨界區的訪問權。此時可以將test函數修改一下,

unsigned int __stdcall test(void *)
{
	while (counter<20)
	{
		while (!TryEnterCriticalSection(&critical)){}
		int num=counter++;
		LeaveCriticalSection(&critical);
		printf("Thread ID : %i; value = %i, is prime = %i\n",GetCurrentThreadId(),num,isPrime(num));
	}
	return 0;
}

這樣做也有問題,會保持進程持續Try,直到獲得臨界區的訪問權,這樣會剝奪其他線程的處理器時間。解決的方法是讓想進入臨界區的線程短暫等待,類似添加了一個等待時間,超時就會離開。兩種方法,一種設置調用EnterCriticalSection的線程進入休眠前旋轉的次數,一種是通過初始化調用InitializeCriticalSectionAndSpinCount()初始化臨界區,參數爲指向臨界區的指針和旋轉的次數,也可以通過調用SetCriticalSectionSpinCount()設置已創建臨界區的旋轉次數。

第二種方案是用互斥量保護代碼段。互斥量是內核對象,所以能在進程之間共享。通過調用CreateMutex或者CreateMutexEx創建互斥量。爲了獲取互斥變量,調用WaitForSingleObject,要麼已經獲得互斥量要麼在指定的超時後返回。線程完成後,調用ReleaseMutex釋放互斥量所保護的代碼段。

#include <Windows.h>
#include <process.h>
#include <stdio.h>
#include <tchar.h>
#include <math.h>
int isPrime(int num)
{
	int i;
	for (i=2;i<(int)(sqrt((float)num)+1.0);i++)
	{
		if (num%i==0)
			return 0;
	}
	return 1;
}
volatile int counter=2;
HANDLE mutex;
unsigned int __stdcall test(void *)
{
	while (counter<20)
	{
		WaitForSingleObject(mutex,INFINITE);
		int num=counter++;
		ReleaseMutex(mutex);
		printf("Thread ID : %i; value = %i, is prime = %i\n",GetCurrentThreadId(),num,isPrime(num));
	}
	return 0;
}
int _tmain(int argc,_TCHAR* argv[])
{
	HANDLE h1,h2;
	mutex=CreateMutex(0,0,0);
	h1=(HANDLE)_beginthreadex(0,0,&test,(void *)0,0,0);
	h2=(HANDLE)_beginthreadex(0,0,&test,(void *)1,0,0);
	WaitForSingleObject(h1,INFINITE);
	WaitForSingleObject(h2,INFINITE);
	CloseHandle(h1);
	CloseHandle(h2);
	getchar();
	CloseHandle(mutex);
	return 0;
}

 

注:volatile與const一樣,volatile是一個類型修飾符(type specifier)。它是被設計用來修飾被不同線程訪問和修改的變量。如果不加入volatile,基本上會導致這樣的結果:要麼無法編寫多線程程序,要麼編譯器失去大量優化的機會。

 

第三種方案是使用輕量級讀寫鎖。鎖在數據庫裏面是一個經常出現的概念,鎖的性質就是允許多個線程對數據具有讀訪問權限,或者單個線程對數據具有寫訪問權限。可以調用InitializeSRWLock()對鎖進行初始化,鎖本質上是用戶變量,不使用內核資源。作爲讀者獲取鎖調用AcquireSRWLockShared(),作爲讀者釋放鎖調用ReleaseSRWLockShared(),寫者獲取鎖調用AcquireSRWLockExclusive(),寫者釋放鎖調用ReleaseSRWLockExclusive()。

#include <Windows.h>
#include <process.h>
#include <stdio.h>
#include <tchar.h>
int array[100][100];
SRWLOCK lock;
unsigned int __stdcall write(void *param)
{
	for (int y=0;y<100;y++)
	{
		for (int x=0;x<100;x++)
		{
			AcquireSRWLockExclusive(&lock);
			array[x][y]++;
			array[y][x]--;
			ReleaseSRWLockExclusive(&lock);
		}
	}
	return 0;
}
unsigned int __stdcall read(void *param)
{
	int value=0;
	for (int y=0;y<100;y++)
	{
		for (int x=0;x<100;x++)
		{
			AcquireSRWLockShared(&lock);
			value=array[x][y]+array[y][x];
			ReleaseSRWLockShared(&lock);
		}
		printf("Value = %i\n",value);
		return value;
	}
}
int  _tmain(int argc,_TCHAR* argv[]){
	HANDLE h1,h2;
	InitializeSRWLock(&lock);
	h1=(HANDLE)_beginthreadex(0,0,&write,(void *)0,0,0);
	h2=(HANDLE)_beginthreadex(0,0,&read,(void *)0,0,0);
	WaitForSingleObject(h1,INFINITE);
	WaitForSingleObject(h2,INFINITE);
	CloseHandle(h1);
	CloseHandle(h2);
	getchar();
	return 0;
}

第四種方案是使用信號量。調用CreateSemaphore和CreateSemaphoreEx可以創建信號量,調用OpenSemaphore可以獲得某個信號量的句柄。信號量是內核對象,創建函數會返回其句柄,調用CloseHandle釋放。信號量通過調用等待函數WaitForSingleObject,其參數爲信號量句柄和超時,返回遞減後的信號量,否則在達到超時後返回。調用ReleaseSemaphore()遞增信號量,其參數爲信號量句柄、信號地增量以及一個指向LONG型變量的可選指針,信號量之前的值寫入該LONG型變量。

給個例子,信號量創建爲最大值爲1、初始值爲1,創建了兩個線程,同時執行相同的代碼,將變量value的值增加200,最終應用程序終止時,變量value的值變爲400.

#include <Windows.h>
#include <process.h>
#include <stdio.h>
#include <tchar.h>
HANDLE semaphore;
int value;
void add(int num)
{
	WaitForSingleObject(semaphore,INFINITE);
	value+=num;
	ReleaseSemaphore(semaphore,1,0);
}
unsigned int __stdcall test(void *)
{
	for (int counter=0;counter<100;counter++)
	{
		add(2);
	}
	return 0;
}

int  _tmain(int argc,_TCHAR* argv[]){
	HANDLE h1,h2;
	value=0;
	semaphore=CreateSemaphore(0,1,1,0);
	h1=(HANDLE)_beginthreadex(0,0,&test,(void *)0,0,0);
	h2=(HANDLE)_beginthreadex(0,0,&test,(void *)0,0,0);
	WaitForSingleObject(h1,INFINITE);
	WaitForSingleObject(h2,INFINITE);
	CloseHandle(h1);
	CloseHandle(h2);
	CloseHandle(semaphore);
	printf("Value = %i\n",value);
	getchar();
	return 0;
}

第五種解決方案是條件變量,需要與臨界區或者輕量級讀寫鎖共同使用,使線程可以進入休眠,直到爲真。比如說,對於生產者——消費者問題,生產者線程負責將數據添加到隊列,消費者線程進入臨界區,從隊列中刪除一個數據。這個問題用條件變量就可以解決。。。

第六種方案就是向其他線程或者進程發出事件完成信號。事件用於向一個或者多個線程發出信號,表明某個事件已經發生。等待某個事件發生的線程將等待該事件對象。完成任務的線程將設置事件爲信號已發送狀態,然後等待線程將被釋放。事件有兩種類型:手動重置和自動重置。事件是內核對象,調用CreateEvent將返回一個句柄,OpenEvent打開現有的事件,SetEvent()將事件設置爲信號已經發出的狀態。自動重置: SetEvent之後,事件自動重置爲未觸發狀態,手動重置: SetEvent之後, 需要調用ResetEvent事件才置爲未觸發狀態。當一個手動重置事件被觸發的時候,正在等待該事件的所有線程都變爲可調度狀態;當一個自動重置事件被觸發的時候,只有一個正在等待該事件的線程會變爲可調度狀態。

給個例子,調用CreateEvent創建事件對象,該對象需要手動重置,且創建爲未發信號狀態。然後創建兩個線程,第一個等待該事件,第二個執行輸出一條消息,然後發送給事件對象。信號使第一個線程繼續執行,並輸出第二條消息。

#include <Windows.h>
#include <process.h>
#include <stdio.h>
#include <tchar.h>
HANDLE event;
unsigned int __stdcall thread1(void *param)
{
	WaitForSingleObject(event,INFINITE);
	printf("Thread 1 done \n");
	return 0;
}
unsigned int __stdcall thread2(void *param)
{
	printf("Thread 2 done \n");
	SetEvent(event);
	return 0;
} int  _tmain(int argc,_TCHAR* argv[]){
	HANDLE h1,h2;
	event=CreateEvent(0,0,0,0);
	h1=(HANDLE)_beginthreadex(0,0,&thread1,0,0,0);
	h2=(HANDLE)_beginthreadex(0,0,&thread2,0,0,0);
	WaitForSingleObject(h1,INFINITE);
	WaitForSingleObject(h2,INFINITE);
	CloseHandle(h2);
	CloseHandle(h1);
	CloseHandle(event);
	getchar();
	return 0;
}




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