1. 程式人生 > 實用技巧 >C/C++ 實現多執行緒與執行緒同步

C/C++ 實現多執行緒與執行緒同步

多執行緒中的執行緒同步可以使用,CreateThread,CreateMutex 互斥鎖實現執行緒同步,通過臨界區實現執行緒同步,Semaphore 基於訊號實現執行緒同步,CreateEvent 事件物件的同步,以及執行緒函式傳遞單一引數與多個引數的實現方式。

CreateThread 實現多執行緒: 先來建立一個簡單的多執行緒例項,無引數傳遞版,執行例項會發現,主執行緒與子執行緒執行無規律。

#include <windows.h>
#include <iostream>

using namespace std;

DWORD WINAPI Func(LPVOID lpParamter)
{
	for (int x = 0; x < 10; x++)
	{
		cout << "thread function" << endl;
		Sleep(200);
	}
	return 0;
}

int main(int argc,char * argv[])
{
	HANDLE hThread = CreateThread(NULL, 0, Func, NULL, 0, NULL);
	CloseHandle(hThread);

	for (int x = 0; x < 10; x++)
	{
		cout << "main thread" << endl;
		Sleep(400);
	}

	system("pause");
	return 0;
}

beginthreadex 實現多執行緒: 這個方法與前面的CreateThread使用完全一致,只是在引數上面應使用void *該引數可以強轉為任意型別,兩者實現效果完全一致。

#include <windows.h>
#include <iostream>
#include <process.h>

using namespace std;

unsigned WINAPI Func(void *arg)
{
	for (int x = 0; x < 10; x++)
	{
		cout << "thread function" << endl;
		Sleep(200);
	}
	return 0;
}

int main(int argc, char * argv[])
{
	HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, Func, NULL, 0, NULL);
	CloseHandle(hThread);
	for (int x = 0; x < 10; x++)
	{
		cout << "main thread" << endl;
		Sleep(400);
	}

	system("pause");
	return 0;
}


CreateMutex 互斥鎖實現執行緒同步: 使用互斥鎖可以實現單位時間內,只允許一個執行緒擁有對共享資源的獨佔,從而實現了互不衝突的執行緒同步。

#include <windows.h>
#include <iostream>

using namespace std;
HANDLE hMutex = NULL;   // 建立互斥鎖

// 執行緒函式
DWORD WINAPI Func(LPVOID lpParamter)
{
	for (int x = 0; x < 10; x++)
	{
		// 請求獲得一個互斥鎖
		WaitForSingleObject(hMutex, INFINITE);
		cout << "thread func" << endl;
		// 釋放互斥鎖
		ReleaseMutex(hMutex);
	}
	return 0;
}

int main(int argc,char * argv[])
{
	HANDLE hThread = CreateThread(NULL, 0, Func, NULL, 0, NULL);

	hMutex = CreateMutex(NULL, FALSE, "lyshark");
	CloseHandle(hThread);

	for (int x = 0; x < 10; x++)
	{
		// 請求獲得一個互斥鎖
		WaitForSingleObject(hMutex, INFINITE);
		cout << "main thread" << endl;
		
		// 釋放互斥鎖
		ReleaseMutex(hMutex);
	}
	system("pause");
	return 0;
}

通過互斥鎖,同步執行兩個執行緒函式。

#include <windows.h>
#include <iostream>

using namespace std;
HANDLE hMutex = NULL;   // 建立互斥鎖
#define NUM_THREAD 50

// 執行緒函式1
DWORD WINAPI FuncA(LPVOID lpParamter)
{
	for (int x = 0; x < 10; x++)
	{
		// 請求獲得一個互斥鎖
		WaitForSingleObject(hMutex, INFINITE);
		cout << "this is thread func A" << endl;
		// 釋放互斥鎖
		ReleaseMutex(hMutex);
	}
	return 0;
}

// 執行緒函式2
DWORD WINAPI FuncB(LPVOID lpParamter)
{
	for (int x = 0; x < 10; x++)
	{
		// 請求獲得一個互斥鎖
		WaitForSingleObject(hMutex, INFINITE);
		cout << "this is thread func B" << endl;
		// 釋放互斥鎖
		ReleaseMutex(hMutex);
	}
	return 0;
}

int main(int argc, char * argv[])
{

	// 用來儲存執行緒函式的控制代碼
	HANDLE tHandle[NUM_THREAD];

	// /建立互斥量,此時為signaled狀態
	hMutex = CreateMutex(NULL, FALSE, "lyshark");

	for (int x = 0; x < NUM_THREAD; x++)
	{
		if (x % 2)
		{
			tHandle[x] = CreateThread(NULL, 0, FuncA, NULL, 0, NULL);
		}
		else
		{
			tHandle[x] = CreateThread(NULL, 0, FuncB, NULL, 0, NULL);
		}
	}

	// 等待所有執行緒函式執行完畢
	WaitForMultipleObjects(NUM_THREAD, tHandle, TRUE, INFINITE);
	
	// 銷燬互斥物件
	CloseHandle(hMutex);

	system("pause");
	return 0;
}


通過臨界區實現執行緒同步: 臨界區與互斥鎖差不多,臨界區使用時會建立CRITICAL_SECTION臨界區物件,同樣相當於一把鑰匙,執行緒函式執行結束自動上交,如下是臨界區函式的定義原型。

//初始化函式原型
VOID InitializeCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
);

//銷燬函式原型
VOID DeleteCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
);

//獲取
VOID EnterCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
);

//釋放
VOID LeaveCriticalSection(  
  LPCRITICAL_SECTION lpCriticalSection
);

這一次我們不適用互斥體,使用臨界區實現執行緒同步,結果與互斥體完全一致,看個人喜好。

#include <windows.h>
#include <iostream>

using namespace std;
CRITICAL_SECTION cs;         // 全域性定義臨界區物件
#define NUM_THREAD 50

// 執行緒函式
DWORD WINAPI FuncA(LPVOID lpParamter)
{
	for (int x = 0; x < 10; x++)
	{
		//進入臨界區
		EnterCriticalSection(&cs);

		cout << "this is thread func A" << endl;

		//離開臨界區
		LeaveCriticalSection(&cs);

	}
	return 0;
}

int main(int argc, char * argv[])
{
	// 用來儲存執行緒函式的控制代碼
	HANDLE tHandle[NUM_THREAD];

	//初始化臨界區
	InitializeCriticalSection(&cs);

	for (int x = 0; x < NUM_THREAD; x++)
	{
		tHandle[x] = CreateThread(NULL, 0, FuncA, NULL, 0, NULL);
	}

	// 等待所有執行緒函式執行完畢
	WaitForMultipleObjects(NUM_THREAD, tHandle, TRUE, INFINITE);
	
	//釋放臨界區
	DeleteCriticalSection(&cs);

	system("pause");
	return 0;
}


Semaphore 基於訊號實現執行緒同步: 通過定義一個訊號,初始化訊號為0,利用訊號量值為0時進入non-signaled狀態,大於0時進入signaled狀態的特性即可實現執行緒同步。

#include <windows.h>
#include <iostream>

using namespace std;

static HANDLE SemaphoreOne;
static HANDLE SemaphoreTwo;

// 執行緒函式1
DWORD WINAPI FuncA(LPVOID lpParamter)
{
	for (int x = 0; x < 10; x++)
	{
		// 臨界區開始時設定 signaled 狀態
		WaitForSingleObject(SemaphoreOne, INFINITE);

		cout << "this is thread func A" << endl;

		// 臨界區結束則設定為 non-signaled 狀態
		ReleaseSemaphore(SemaphoreOne, 1, NULL);
	}
	return 0;
}

// 執行緒函式2
DWORD WINAPI FuncB(LPVOID lpParamter)
{
	for (int x = 0; x < 10; x++)
	{
		// 臨界區開始時設定 signaled 狀態
		WaitForSingleObject(SemaphoreTwo, INFINITE);

		cout << "this is thread func B" << endl;

		// 臨界區結束則設定為 non-signaled 狀態
		ReleaseSemaphore(SemaphoreTwo, 1, NULL);
	}
	return 0;
}

int main(int argc, char * argv[])
{
	// 用來儲存執行緒函式的控制代碼
	HANDLE hThreadA, hThreadB;

	// 建立訊號量物件,並且設定為0進入non-signaled狀態 
	SemaphoreOne = CreateSemaphore(NULL, 0, 1, NULL);

	// 建立訊號量物件,並且設定為1進入signaled狀態
	SemaphoreTwo = CreateSemaphore(NULL, 1, 1, NULL);       // 先執行這一個執行緒函式

	hThreadA = CreateThread(NULL, 0, FuncA, NULL,0, NULL);
	hThreadB = CreateThread(NULL, 0, FuncB, NULL, 0, NULL);

	// 等待兩個執行緒函式執行完畢
	WaitForSingleObject(hThreadA, INFINITE);
	WaitForSingleObject(hThreadA, INFINITE);

	// 銷燬兩個執行緒函式
	CloseHandle(SemaphoreOne);
	CloseHandle(SemaphoreTwo);

	system("pause");
	return 0;
}

上面的一段程式碼,容易產生死鎖現象,即,執行緒函式B執行完成後,A函式一直處於等待狀態。

執行WaitForSingleObject(semTwo, INFINITE);會讓執行緒函式進入類似掛起的狀態,當接到ReleaseSemaphore(semOne, 1, NULL);才會恢復執行。

#include <windows.h>  
#include <stdio.h>  

static HANDLE semOne,semTwo;
static int num;

// 執行緒函式A用於接收參書
DWORD WINAPI ReadNumber(LPVOID lpParamter)
{
	int i;
	for (i = 0; i < 5; i++)
	{
		fputs("Input Number: ", stdout);
		//臨界區的開始 signaled狀態  
		WaitForSingleObject(semTwo, INFINITE);
		
		scanf("%d", &num);

		//臨界區的結束 non-signaled狀態  
		ReleaseSemaphore(semOne, 1, NULL);
	}
	return 0;
}

// 執行緒函式B: 使用者接受引數後完成計算
DWORD WINAPI Check(LPVOID lpParamter)
{
	int sum = 0, i;
	for (i = 0; i < 5; i++)
	{
		//臨界區的開始 non-signaled狀態  
		WaitForSingleObject(semOne, INFINITE);
		sum += num;
		//臨界區的結束 signaled狀態  
		ReleaseSemaphore(semTwo, 1, NULL);
	}
	printf("The Number IS: %d \n", sum);
	return 0;
}

int main(int argc, char *argv[])
{
	HANDLE hThread1, hThread2;

	//建立訊號量物件,設定為0進入non-signaled狀態  
	semOne = CreateSemaphore(NULL, 0, 1, NULL);

	//建立訊號量物件,設定為1進入signaled狀態  
	semTwo = CreateSemaphore(NULL, 1, 1, NULL);

	hThread1 = CreateThread(NULL, 0, ReadNumber, NULL, 0, NULL);
	hThread2 = CreateThread(NULL, 0, Check, NULL, 0, NULL);

	// 關閉臨界區
	WaitForSingleObject(hThread1, INFINITE);
	WaitForSingleObject(hThread2, INFINITE);

	CloseHandle(semOne);
	CloseHandle(semTwo);

	system("pause");
	return 0;
}


CreateEvent 事件物件的同步: 事件物件實現執行緒同步,與前面的臨界區和互斥體有很大的不同,該方法下建立物件時,可以在自動non-signaled狀態執行的auto-reset模式,當我們設定好我們需要的引數時,可以直接使用SetEvent(hEvent)設定事件狀態,會自動執行執行緒函式。

#include <windows.h>  
#include <stdio.h>  
#include <process.h>  
#define STR_LEN 100  

// 儲存全域性字串
static char str[STR_LEN];

// 設定事件控制代碼
static HANDLE hEvent;

// 統計字串中是否存在A
unsigned WINAPI NumberOfA(void *arg)
{
	int cnt = 0;
	// 等待執行緒物件事件
	WaitForSingleObject(hEvent, INFINITE);
	for (int i = 0; str[i] != 0; i++)
	{
		if (str[i] == 'A')
			cnt++;
	}
	printf("Num of A: %d \n", cnt);
	return 0;
}

// 統計字串總長度
unsigned WINAPI NumberOfOthers(void *arg)
{
	int cnt = 0;
	// 等待執行緒物件事件
	WaitForSingleObject(hEvent, INFINITE);
	for (int i = 0; str[i] != 0; i++)
	{
		if (str[i] != 'A')
			cnt++;
	}
	printf("Num of others: %d \n", cnt - 1);
	return 0;
}

int main(int argc, char *argv[])
{
	HANDLE hThread1, hThread2;

	// 以non-signaled建立manual-reset模式的事件物件
	// 該物件建立後不會被立即執行,只有我們設定狀態為Signaled時才會繼續
	hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

	hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);
	hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);

	fputs("Input string: ", stdout);
	fgets(str, STR_LEN, stdin);

	// 字串讀入完畢後,將事件控制代碼改為signaled狀態  
	SetEvent(hEvent);

	WaitForSingleObject(hThread1, INFINITE);
	WaitForSingleObject(hThread2, INFINITE);

	//non-signaled 如果不更改,物件繼續停留在signaled
	ResetEvent(hEvent);

	CloseHandle(hEvent);

	system("pause");
	return 0;
}


執行緒函式傳遞單個引數: 執行緒函式中的定義中LPVOID允許傳遞一個引數,只需要在縣城函式中接收並強轉(int)(LPVOID)port即可。

#include <stdio.h>
#include <Windows.h>

// 執行緒函式接收一個引數
DWORD WINAPI ScanThread(LPVOID port)
{
	// 將引數強制轉化為需要的型別
	int Port = (int)(LPVOID)port;
	printf("[+] 埠: %5d \n", port);
	return 1;
}

int main(int argc, char* argv[])
{
	HANDLE handle;

	for (int port = 0; port < 100; port++)
	{
		handle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ScanThread, (LPVOID)port, 0, 0);
	}
	WaitForSingleObject(handle, INFINITE);

	system("pause");
	return 0;
}


執行緒函式傳遞多引數: 如果想線上程函式中傳遞多個引數,則需要傳遞一個結構指標,通過執行緒函式內部強轉為結構型別後,取值,這個案例花費了我一些時間,網上也沒找到合適的解決方法,或找到的都是歪瓜裂棗瞎轉的東西,最後還是自己研究了一下寫了一個沒為題的。

其主要是執行緒函式中呼叫的引數會與下一個執行緒函式結構相沖突,解決的辦法時在每次進入執行緒函式時,自己拷貝一份,每個人使用自己的那一份,才可以避免此類事件的發生,同時最好配合執行緒同步一起使用,如下時執行緒掃描器的部分程式碼片段。

#include <stdio.h>
#include <windows.h>

typedef struct _THREAD_PARAM
{
	char *HostAddr;             // 掃描主機
	DWORD dwStartPort;          // 埠號
}THREAD_PARAM;


// 這個掃描執行緒函式
DWORD WINAPI ScanThread(LPVOID lpParam)
{
	// 拷貝傳遞來的掃描引數
	THREAD_PARAM ScanParam = { 0 };

	// 這一步很重要,如不拷貝,則會發生重複賦值現象,導致掃描埠一直都是一個。
	// 坑死人的玩意,一開始我始終沒有發現這個問題。sb玩意!!
	MoveMemory(&ScanParam, lpParam, sizeof(THREAD_PARAM));

	printf("地址: %-16s --> 埠: %-5d 狀態: [Open] \n", ScanParam.HostAddr, ScanParam.dwStartPort);
	return 0;
}

int main(int argc, char *argv[])
{
	THREAD_PARAM ThreadParam = { 0 };
	ThreadParam.HostAddr = "192.168.1.10";

	for (DWORD port = 1; port < 100; port++)
	{
		ThreadParam.dwStartPort = port;
		HANDLE hThread = CreateThread(NULL, 0, ScanThread, (LPVOID)&ThreadParam, 0, NULL);
		WaitForSingleObject(hThread, INFINITE);
	}

	system("pause");
	return 0;
}