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

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