TCP/IP網絡編程_基於Windows的編程_第20章Windows中的線程同步

在這裏插入圖片描述

20.1 同步的分類及 CRITICAL_SECTION 同步

Windows 中存在多種同步技術, 它們的基本概念大同小異, 相互間也有一定聯繫, 所以不難掌握.

用戶模式(User mode) 和內核模式(Kernal mode)

Windows 操作系統的運行方式(程序運行方式) 是 “雙模式操作(Dual-mode Operation)”, 這意味着 Windows 在運行過程中存在如下2種模式.
在這裏插入圖片描述
內核是操作系統的核心模塊, 可以簡單定義如下形式.
在這裏插入圖片描述
實際上, 在應用程序運行過程中, Windows 操作系統不會一直停留在用戶模式, 而是在用戶模式和內核模式之間切換. 例如, 各位可以在 Windows 中創建線程. 雖然創建線程的請求時由應用程序的函數調用完成, 但實際創建線程是操作系統. 因此, 創建線程的過程中無法避免向內核模式的轉換.

定義這2種模式主要是爲了提高安全性. 應用程序的運行時錯誤會破壞操作系統及各種資源. 特別是 C/C++ 可以進行指針操作運算, 很容易發生這類問題. 例如, 因爲錯誤的指針運算覆蓋了操作系統中存有重要的內存區域, 這很可能引起操作系統崩潰. 但實際上各位從未經歷過這類事件, 因爲用戶模式會保護與操作系統有關的內存區域. 因此, 即使遇到錯誤的指針運算也僅停止應用程序的運行, 而不會影響操作系統. 總之, 像線程這種伴隨着內核對象創建的資源創建過程中, 都要默認經歷如下 模式轉換過程:
在這裏插入圖片描述
從用戶模式切換到內核模式是爲了創建資源, 從內核模式再次切換到用戶模式是爲了執行應用程序的剩餘部分. 不僅是資源的創建, 與內核對象有關的所有實務都在內核模式下進行. 模式切換對系統而言是一種負擔, 頻繁的模式切換會影響性能.

用戶模式同步

用戶模式同步是用戶模式下進行的同步, 即無需操作系統的幫助而在應用程序級別進行的同步. 用戶模式同步的最大優點是–速度快. 無需切換到內核模式, 僅考慮這一點也比經歷內核模式切換的其他方法要快. 而且使用方法相對簡單, 因此, 適當運用用戶模式同步並無壞處. 但因爲這種同步方法不會藉助操作系統的力量, 其功能上也存在一定的侷限. 稍後將介紹屬於用戶模式同步, 基於 “CRITICAL_SECTION” 的同步方法.

內核模式同步

前面已介紹過用戶模式同步, 即使不另說明, 相信各位也能大概說出內核模式同步的特性及優缺點. 下面給出內核模式同步的優點.
在這裏插入圖片描述
因爲都是通過操作系統的幫助完成同步的, 所以提供更多的功能. 特別是在內核模式同步中, 可以跨越進程線程同步. 與此同時, 由於無法避免用戶模式之間的切換, 所以性能上會受到一定影響.

大家此時很可能想到: “因爲是基於內核對象的操作, 所以可以進行不同進程間的同步!” 因爲內核對象並不屬於某一進程, 而是操作系統擁有並管理的.
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

基於 CRITICAL_SECTION 的同步

基於CRITICAL_SECTION的同步中將創建並運用 ‘’CRITICAL_SECTION 對象“, 但這並非內核對像, 與其他同步對象相同, 它是進入臨界區的一把鑰匙(key). 因此, 爲了進入臨界區, 需要得到 CRITICAL_SECTION 對象這把 “鑰匙”. 相反, 離開時應上交 CRITICAL_SECTIO 對象(以下簡稱CS). 下面介紹CS對象的初始化及銷燬相關函數.
在這裏插入圖片描述
上述函數的參數類型 LPCRITLCAL_SECTION 是 CRITICAL_SECTION 指針類型. 另外 DeleteCriticalSection 並不是銷燬 CRITICAL_SECTION 對象的函數. 該函數的作用是銷燬 CRITICAL_SECTIO 對象使用過的(CRITICAL_SECTION 對象相關的)資源. 接下來介紹獲取(擁有者) 及釋放CS 對象的函數, 可以簡單理解爲獲取和釋放"鑰匙" 的函數.
在這裏插入圖片描述
與 Linux 部分中介紹過的互斥量類似, 相信大部分人僅靠這些函數介紹也能寫出示例程序(我的個人經驗). 下面利用 CS 對象將第19章的示例 thread3_win.c 改爲同步程序.

在這裏插入圖片描述

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

#define NUM_THREAD 50
unsigned WINAPI threadInc(void* arg);
unsigned WINAPI threadDes(void* arg);

long long num = 0;
CRITICAL_SECTION cs;

int main(int argc, char* argv[])
{
	HANDLE tHandles[NUM_THREAD];
	int i;

	InitializeCriticalSection(&cs);
	for (i = 0; i < NUM_THREAD; i++)
	{
		if (i % 2)
		{
			tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
		}
		else
		{
			tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
		}
	}

	WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
	DeleteCriticalSection(&cs);
	printf("result: %lld \n", num);
	return 0;
}

unsigned WINAPI threadInc(void* arg)
{
	int i;
	EnterCriticalSection(&cs);
	for (i = 0; i < 50000000; i++)
	{
		num += 1;
	}
	LeaveCriticalSection(&cs);

	return 0;
}

unsigned WINAPI threadDes(void* arg)
{
	int i;
	EnterCriticalSection(&cs);
	for (i = 0; i < 50000000; i++)
	{
		num -= 1;
	}
	LeaveCriticalSection(&cs);

	return 0;
}

在這裏插入圖片描述
在這裏插入圖片描述
程序中將整個循環納入臨界區, 主要是爲了減少運行時間. 如果只將訪問 num 的語句納入臨界區, 那將不知何時才能得到運行結果(如果時間充裕可以試試, 但運行時間長得讓人覺得是否發生了死鎖), 因爲這將導致大量獲取和釋放 CS 對象. 另外, 上述示例僅僅是爲了學習同步機制編寫的, 沒有任何現實意義(如此編寫程序的情況本身並不現實).

20.2 內核模式的同步方法

典例的內核模式同步方法基於事件(Event), 信號量, 互斥量等內核對象的同步, 下面從互斥量開始逐一介紹.

基於互斥量(Mutual Exclusion) 對象的同步

基於互斥量對象的同步方法與基於CS對象的同步方法類型, 因此, 互斥量對象同樣可以理解爲"鑰匙". 首先介紹創建互斥量對象的函數.
在這裏插入圖片描述
從上述參數說明中可以看到, 如果互斥量對象不屬於任何擁有者, 則將進入 signaled 狀態. 利用該函數特點進行同步. 另外, 互斥量屬於內核對象, 所以通過如下函數銷燬.
在這裏插入圖片描述
上述函數是銷燬內核對象的函數, 所以同樣可以銷燬即將介紹的信號量及事件. 下面介紹獲取和釋放互斥量的函數, 但我認爲只需要釋放的函數, 因爲獲取是通過各位熟悉的 WaitForSingleObject 函數完成的.
在這裏插入圖片描述
接下來分析獲取和釋放互斥量的過程. 互斥量被某一線程獲取時(擁有時), 爲signaled 狀態, 釋放時(未擁有時)進入signaled 狀態. 因此, 可以使用 WaitForSingleObject 函數驗證互斥量是否已分配. 該函數的調用結果有如下2種.
在這裏插入圖片描述
互斥量在 WaitForSingleObject 函數返回時自動進入 non-signale 狀態, 因爲它是第19章介紹過的 “auto-reset” 模式的內核對象. 結果, waitForSingleObject 函數成爲申請互斥量時調用的函數. 因此, 基於互斥量的臨界區保護代碼如下.
在這裏插入圖片描述
WaitForSingleObject 函數使互斥量進入 non-signale 狀態, 限制訪問臨界區, 所以相當於臨界區的門禁系統. 相反, ReleaseMutex 函數使互斥量重新進入 signal 狀態, 所以相當於臨界區的出口. 下面將之前介紹過的 SyncCS_win.c 示例改爲互斥量對象的實現方式. 更改後的程序與 SyncCS_win.c 沒有太大區別, 故省略相關說明.
在這裏插入圖片描述

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

#define NUM_THREAD 50

unsigned WINAPI threadInc(void* arg);
unsigned WINAPI threadDes(void* arg);

long long num = 0;
HANDLE hMutex;

int main(int argc, char* argv[])
{
	HANDLE tHandles[NUM_THREAD];
	int i;

	hMutex = CreateMutex(NULL, FALSE, NULL);
	for (i = 0; i < NUM_THREAD; i++)
	{
		if (i % 2)
		{
			tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
		}
		else
		{
			tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
		}
	}

	WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
	CloseHandle(hMutex);
	printf("return: %lld \n", num);

	return 0;
}

unsigned WINAPI threadInc(void* arg)
{
	int i;
	WaitForSingleObject(hMutex, INFINITE);
	for (i = 0; i < 50000000; i++)
	{
		num += 1;
	}
	ReleaseMutex(hMutex);

	return 0;
}

unsigned WINAPI threadDes(void* arg)
{
	int i;
	WaitForSingleObject(hMutex, INFINITE);
	for (i = 0; i < 50000000; i++)
	{
		num -= 1;
	}
	ReleaseMutex(hMutex);

	return 0;
}

在這裏插入圖片描述
在這裏插入圖片描述

基於信號量對象的同步

Windows 中基於信號量對象的同步也與 Linux 下的信號量類型, 二者都是利用名爲 “信號量值” (Semaphore Value) 的整數值完全同步的, 而且該值都不能小於0. 當然, Windows 的信號量值註冊與內核對象.

下面介紹傳創建信號量的函數, 當然, 其銷燬同樣是利用 CloseHandle 函數進行的.
在這裏插入圖片描述
可以利用 "信號量值爲0時進入 non-signaled 狀態, 大於0時進入signale狀態"的特性進行同步. 向 IInitialCount 參數傳遞0時, 創建non-signaled 狀態的信號量對象. 而向 IMaximumCount 傳遞3時, 信號量最大值爲3, 因此可以實現3個線程同時訪問臨界區時的同步. 下面介紹釋放信號量對象的函數.
在這裏插入圖片描述
信號量對象的值大於0時成爲 signaled 狀態, 爲0時成爲 non-signaled 狀態. 因此, 調用WaitForSingleObject函數時, 信號量大於0的情況下才會返回的同時將信號量值減1, 同時進入 non-signaled 狀態(當然, 僅限於信號量減1後等於0的情況). 可以通過如下程序結構保護臨界區.
在這裏插入圖片描述
下面給出信號量對象相關示例, 該示例只是第18章 semaphore.c 的Windows 移植版. 關於程序流說明參考之前的內容, 本示例中主要補充說明對象調用同步函數的部分.
在這裏插入圖片描述

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

unsigned WINAPI Read(void* arg);
unsigned WINAPI Accu(void* arg);

static HANDLE semOne;
static HANDLE semTwo;
static int num;

int main(int argc, char* argv[])
{
	HANDLE hThread1, hThread2;
	semOne = CreateSemaphore(NULL, 0, 1, NULL);
	semTwo = CreateSemaphore(NULL, 1, 1, NULL);

	hThread1 = (HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);
	hThread2 = (HANDLE)_beginthreadex(NULL, 0, Accu, NULL, 0, NULL);

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

	CloseHandle(semOne);
	CloseHandle(semTwo);

	return 0;
}

unsigned WINAPI Read(void* arg)
{
	int i;
	for (i = 0; i < 5; i++)
	{
		fputs("Input num: ", stdout);
		WaitForSingleObject(semTwo, INFINITE);
		scanf("%d", &num);
		ReleaseSemaphore(semOne, 1, NULL);
	}

	return 0;
}

unsigned WINAPI Accu(void* arg)
{
	int sum = 0, i;
	for (i = 0; i < 5; i++)
	{
		WaitForSingleObject(semOne, INFINITE);
		sum += num;
		ReleaseSemaphore(semTwo, 1, NULL);
	}
	printf("Result: %d \n", sum);
	return 0;
}

在這裏插入圖片描述
在這裏插入圖片描述

基於事件對象的同步

事件同步對象與前2種同步方法相比有很大不同, 區別就在於, 該方法下創建對象時, 可以在自動方法相比有很大不同, 區別就在於, 該方式下創建對象時, 可以在自動以 non-signaled 狀態運行的auto-reset 模式和與之相反的 manual-reset 模式中任選其一. 而事件對象的主要特點是可以創建 manual-rese 模式的對象, 我也將對此進行重點講解. 首先介紹用於創建事件對象的函數.
在這裏插入圖片描述
相信各位也發現了, 上述函數中需要重點關注的是第二個參數. 傳入 TRUE 時創建 manual-reset 模式的事件對象, 此時即使 WaitForSingleObject 函數返回也不會回到 non-signale 狀態. 因此, 在這種情況下, 需要通過如下2個函數明確更改對象狀態.
在這裏插入圖片描述
傳遞事件對象句柄並希望改爲 non-signaled 狀態時, 應調用 ResetEvent 函數. 如果希望改爲 signaled 狀態, 則可以調用 SetEvent 函數. 通過如下示例介紹事件的具體使用方法, 該示例中的2個線程將同時等待輸入字符串.
在這裏插入圖片描述

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

#define STR_LEN 100

unsigned WINAPI NumberOfA(void* arg);
unsigned WINAPI NumberOfOthres(void* arg);

static char str[STR_LEN];
static HANDLE hEvent;

int main(int argc, char* argv[])
{
	HANDLE hThread1, hThread2;
	hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
	hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);
	hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthres, NULL, 0, NULL);

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

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

	ResetEvent(hEvent);
	CloseHandle(hEvent);
	return 0;
}

unsigned WINAPI NumberOfA(void* arg)
{
	int i, cnt = 0;
	WaitForSingleObject(hEvent, INFINITE);
	for (i = 0; str[i] != 0; i++)
	{
		if (str[i] == 'A')
		{
			cnt++;
		}
	}
	printf("Num of A: %d \n", cnt);
	return 0;
}

unsigned WINAPI NumberOfOthres(void* arg)
{
	int i, cnt = 0;
	WaitForSingleObject(hEvent, INFINITE);
	for (i = 0; str[i] != 0; i++)
	{
		if (str[i] != 'A')
		{
			cnt++;
		}
	}
	
	printf("Num of others: %d \n", cnt - 1);
	return 0;
}

在這裏插入圖片描述
在這裏插入圖片描述
上述簡單示例演示的是2個線程同時退出等待狀態的情景. 在這種情況下, 以 manual-reset 模式創建的事件對象應該是更好的選擇.

20.3 windows 平臺下實現多線程服務器端

第18章講完線程的創建和同步方法後, 最終實現了多線程聊天服務器端和客戶端. 按照這種順序, 本章最後也將在Windows 平臺下實現聊天服務器端和客戶端. 首先給出聊天服務器端的源碼. 該程序是第18章 chat_serv.c 的Windows移植版, 故省略其說明.
在這裏插入圖片描述

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <Windows.h>
#include <process.h>

#define BUF_SIZE 100
#define MAX_CLNT 256

unsigned WINAPI HandleClnt(void* arg);
void SendMsg(char* msg, int len);
void ErrorHandling(const char* msg);

int clntCnt = 0;
SOCKET clntSocks[MAX_CLNT];
HANDLE hMutex;

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	SOCKADDR_IN servAdr, clntAdr;
	int clntAdrSz;
	HANDLE hThread;
	if (argc != 2)
	{
		printf("Usage : %s  <port> \n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error");
	}

	hMutex = CreateMutex(NULL, FALSE, NULL);
	hServSock = socket(PF_INET, SOCK_STREAM, 0);

	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAdr.sin_port = htons(atoi(argv[1]));

	if (bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
	{
		ErrorHandling("bind() error");
	}

	if (listen(hServSock, 5) == SOCKET_ERROR)
	{
		ErrorHandling("listen() error");
	}

	while (1)
	{
		clntAdrSz = sizeof(clntAdr);
		hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSz);

		WaitForSingleObject(hMutex, INFINITE);
		clntSocks[clntCnt++] = hClntSock;
		ReleaseMutex(hMutex);

		hThread = (HANDLE)_beginthreadex(NULL, 0, HandleClnt, (void*)&hClntSock, 0, NULL);
		printf("Connected client IP: %s \n", inet_ntoa(clntAdr.sin_addr));
	}

	closesocket(hServSock);
	WSACleanup();
	return 0;
}

unsigned WINAPI HandleClnt(void* arg)
{
	SOCKET hClntSock = *((SOCKET*)arg);
	int strLen = 0, i;
	char msg[BUFSIZ];

	while ((strLen == recv(hClntSock, msg, sizeof(msg), 0)) != 0)
	{
		SendMsg(msg, strLen);
	}

	WaitForSingleObject(hMutex, INFINITE);
	for (i = 0; i < clntCnt; i++)
	{
		if (hClntSock == clntSocks[i])
		{
			while (i++ < clntCnt - 1)
			{
				clntSocks[i] = clntSocks[i + 1];
			}
			break;
		}
	}
	clntCnt--;
	ReleaseMutex(hMutex);
	closesocket(hClntSock);
	return 0;
}

void SendMsg(char* msg, int len)
{
	int i;
	WaitForSingleObject(hMutex, INFINITE);
	for (i = 0; i < clntCnt; i++)
	{
		send(clntSocks[i], msg, len, 0);
	}
	ReleaseMutex(hMutex);
}


void ErrorHandling(const char* msg)
{
	fputs(msg, stderr);
	fputc('\n', stderr);
	exit(1);
}

下面介紹聊天 客戶端. 該示例是第18章的 chat_clnt.c 的 Windows 移植版, 故同樣省略其說明.
在這裏插入圖片描述

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <string.h>
#include <process.h>

#define BUF_SIZE  100
#define NAME_SIZE 20

unsigned WINAPI SendMsg(void* arg);
unsigned WINAPI RecvMsg(void* arg);
void  ErrorHandling(const char* msg);

char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUFSIZ];

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSock;
	SOCKADDR_IN servAdr;
	HANDLE hSndThread, hRcvThread;
	if (argc != 4)
	{
		printf("Usage : %s <IP> <port> <name> \n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error");
	}

	sprintf(name, "[%s]", argv[3]);
	hSock = socket(PF_INET, SOCK_STREAM, 0);

	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.s_addr = inet_addr(argv[1]);
	servAdr.sin_port = htons(atoi(argv[2]));

	if (connect(hSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
	{
		ErrorHandling("connect() error");
	}

	hSndThread = (HANDLE)_beginthreadex(NULL, 0, SendMsg, (void*)&hSock, 0, NULL);
	hSndThread = (HANDLE)_beginthreadex(NULL, 0, RecvMsg, (void*)&hSock, 0, NULL);

	WaitForSingleObject(hSndThread, INFINITE);
	WaitForSingleObject(hRcvThread, INFINITE);
	closesocket(hSock);
	WSACleanup();
	
	return 0;
}

unsigned WINAPI SendMsg(void* arg)
{
	SOCKET hSock = *((SOCKET*)arg);
	char nameMsg[NAME_SIZE + BUF_SIZE];
	while (1)
	{
		fgets(msg, BUF_SIZE, stdin);
		if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n"))
		{
			closesocket(hSock);
			exit(0);
		}
		sprintf(nameMsg, "%s %s", name, msg);
		send(hSock, nameMsg, strlen(nameMsg), 0);
	}
	return 0;
}

unsigned WINAPI RecvMsg(void* arg)
{
	int hSock = *((SOCKET*)arg);
	char nameMsg[NAME_SIZE + BUF_SIZE];
	int strLen;
	while (1)
	{
		strLen = recv(hSock, nameMsg, NAME_SIZE + BUF_SIZE - 1, 0);
		if (strLen == -1)
		{
			return -1;
		}
		nameMsg[strLen] = 0;
		fputs(nameMsg, stdout);
	}

	return 0;
}

void  ErrorHandling(const char* msg)
{
	fputs(msg, stderr);
	fputc('\n', stderr);
	exit(1);
}

運行同樣與 char_server.c , char_clnt.c 的運行結果相同, 故省略. 之前已省略了不少內容,
作者感到很抱歉.

結語:

你可以下面這個網站下載這本書<TCP/IP網絡編程>
https://www.jiumodiary.com/

時間: 2020-06-16

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